XMPP Bots
|
This content was generated with the assistance of AI and should be verified against the official documentation of each tool and framework before being relied on in production. This section’s bibliography lists the reference material consulted while preparing these pages. |
This page was written on 2026-10-01 against slixmpp 1.17.0 (Python 3.12) with the LangChain 1.4.3 core, Smack 4.5.0 (Java 21) with the Spring AI 2.0.1 core on Spring Boot 4.1.1, ejabberd 26.09, Openfire 5.1.2 and the current Prosody documentation (see the version baseline). The snippets are illustrative: they were written from the official slixmpp and Smack documentation and are not compiled or run by this repository. The slixmpp plugin and method names and the Smack class and method names were checked against the official API documentation on that date; server configuration keys are summarized from memory of the vendors' documentation and must be checked against the version you deploy. XEP statuses in the table below were read from the XSF index on the same date.
XMPP (the Extensible Messaging and Presence Protocol) is the oldest protocol in this sub-section and the only one that you can federate and self-host end to end: your assistant is just another account, or another service, on a chat server that you or your organisation run. It is a good fit for internal tooling, for communities that already live on XMPP and for situations where the data must not leave your infrastructure. It has no rich cards and no native token streaming, so the adapter leans on three small extensions: chat states for a typing indicator, message correction to grow one message as tokens arrive, and the archive for history. The same channel-agnostic core described in Channel-Agnostic Architecture sits behind it. For how XMPP compares with the other wire protocols used for messaging, see Messaging Protocols.
XMPP in five minutes
JIDs
Every entity has a Jabber ID (JID), written localpart@domainpart/resourcepart. The bare JID docs-bot@chat.example.com
identifies an account; the resource (/laptop, /phone, /worker-1) identifies one connected session of that account.
Routing for a bare JID goes to the most suitable connected resource, so a person can be signed in on two devices at the
same time. For the adapter this gives a ready-made identity: the bare JID of the sender is the userId, and the pair
(user bare JID, bot bare JID) or the room JID is the conversationId. The server authenticates the sender; the bot only
needs to trust the from attribute of stanzas delivered by its own server, which is what makes XMPP identity mapping
simpler than on most business channels. The access-control model from
RAG Access Control & Privacy applies unchanged on top of that
userId.
Stanzas
All traffic is XML stanzas sent over one long-lived stream (RFC 6120). There are three kinds:
| Stanza | Role for a bot |
|---|---|
|
Chat text. The |
|
Availability ( |
|
Request and response, used for service discovery (XEP-0030), archive queries (XEP-0313), pings and roster fetches. |
The base semantics of messaging and presence are in RFC 6121; the core transport, TLS and SASL are in RFC 6120. Both are listed in the references.
Presence
A bot that does not send presence receives little or nothing. After the session starts it sends one <presence/>, and it
may later publish a status (dnd while a long ingestion job runs, for example). Contacts that want to see the bot’s
availability subscribe to it; accepting subscriptions automatically is acceptable for a public assistant and a bad idea
for an internal one, where the server’s roster or allow-list should decide who can talk to the bot at all.
Servers
All three servers below are mature, support the extensions used on this page and can be self-hosted; the choice is mostly operational.
| Server | Written in / configured by | Notes for a bot |
|---|---|---|
ejabberd 26.09 |
Erlang; one YAML file |
Scales to very large clusters. Built-in MUC and MAM modules. External components connect through a |
Prosody |
Lua; a Lua configuration file with modular plugins |
Light to run and easy to extend. MUC and MAM are enabled through module lists, and external components are declared as
|
Openfire 5.1.2 |
Java; web admin console and plugins |
Convenient when the team prefers a GUI and a JVM stack. The console manages users, MUC rooms and external components, and the Ignite Realtime project maintains Smack, the Java library used below, alongside it. |
For development, any of them can run in a container with a self-signed certificate and a user created from the command line or console; for production, use a real certificate for the domain (see security).
Client bot or component
There are two ways to attach a bot to a server.
| Client-account bot | Component (XEP-0114) | |
|---|---|---|
What it is |
An ordinary account, for example |
A service that owns a whole sub-domain, for example |
Addressing |
One bare JID; people add it to their contact list or invite it to rooms. |
Any JID under the sub-domain ( |
Server cooperation |
None beyond an account. Works on any server, including hosted ones that allow bot accounts. |
The server operator must register the component and its secret, so it only suits servers you control. |
Features |
Presence, roster, carbons, stream management, MAM of its own conversations. |
Raw stanza routing; the server does not manage roster or archive for the component, so those are yours to build. |
Best fit |
DocsAssistant as one visible contact or a room participant. |
Gateways and multi-tenant services where each customer gets a JID. |
XEP-0114 (Jabber Component Protocol) is classified by the XSF as a historical specification, but it remains the interface
that ejabberd, Prosody and Openfire all offer for external components. slixmpp provides a ComponentXMPP class (it takes
the component JID, the secret, and the server host and port) for that mode. Because DocsAssistant needs one identity and
benefits from roster, carbons and archive, a client-account bot is the default recommendation, and the rest of this page
uses it.
Conversation shape
A single exchange between a person and the bot looks like this, using the extensions described below:
The pieces, in order of importance for a bot, are below.
One-to-one chats and rooms (XEP-0045)
A one-to-one chat is a <message type="chat"> to the bot’s JID. A room is a Multi-User Chat (XEP-0045): the bot joins
docs@conference.chat.example.com with a nickname by sending presence to room/nick, then receives
type="groupchat" messages from room/othernick. Three adapter rules follow:
-
Answer only when addressed. In a room, people talk to each other. Respond only to messages that mention the bot’s nickname or start with a command prefix, and never respond to your own echoes (the room reflects the bot’s messages back with its own occupant JID).
-
Identity is the occupant, not the room. In a room the visible sender is
room/nick; the real bare JID is visible to every occupant in a non-anonymous room and only to moderators in a semi-anonymous one. Use the room JID plus the nickname as the conversation key, and treat authorization in rooms as a coarser policy (everyone in the room sees the answer) than in one-to-one chats. -
Be careful with data the room has not earned. An answer drawn from restricted documents must never be posted to a room whose members lack the access; filter retrieval by the lowest common privilege, or answer privately by sending the user a one-to-one message with a short notice in the room.
Chat states while generating (XEP-0085)
Generation latency is the main user-experience problem of a chat bot. XEP-0085 defines five states carried as child
elements of a message: active, composing, paused, inactive and gone. Send composing as soon as a question is
accepted, optionally refresh it every few seconds for long answers, and send active with the final message (or with the
last correction) to clear the indicator. The state costs one tiny stanza and works in virtually every client; it is the
direct XMPP equivalent of the typing indicators discussed in
Agent UX. A chat state message must have no body of its own.
Pseudo-streaming with message correction (XEP-0308)
XMPP has no way to stream into a message. XEP-0308 (Last Message Correction) is the closest standard tool: the sender
transmits a new message with a <replace xmlns="urn:xmpp:message-correct:0" id="…"/> element whose id is the id of the
original message, and clients that understand it update the displayed message in place. Three rules from the
specification matter for implementation:
-
only the original sender can correct, so the bot corrects its own messages;
-
every correction refers to the id of the first message, not to the previous correction;
-
clients that do not support it show each correction as a new message, so a bot that edits dozens of times will spam those clients.
The last point is why edits must be rate limited. If an answer is generated in \( T \) seconds and the bot edits at most once per \( \Delta \) seconds, the number of stanzas it sends per answer is
\[ N_{\text{edits}} = \left\lceil \frac{T}{\Delta} \right\rceil + 1 , \]
where the extra stanza is the first message. With \( T = 12 \) s and \( \Delta = 1.5 \) s that is nine stanzas, which displays smoothly and stays far below typical per-user message rate limits on servers; editing per token (hundreds of stanzas) trips those limits and can get the account throttled or disconnected. Practical rules: buffer tokens, send the first message after the first sentence or about one second, edit no more often than once per second or so, always send a final correction with the complete text and citations, and provide a configuration switch to fall back to a single final message for clients or rooms where corrections are not honoured. Whether a given server allows a correction in a room, and how long its archive keeps both versions, depends on the server and client; test with the clients your users have.
Message history (XEP-0313)
Message Archive Management lets an entity query the server-side archive with <iq> requests, filtered by time and by
conversation partner, and receive the results as forwarded messages in pages. DocsAssistant has two uses for it. The first
is continuity: after a restart, the bot can reload the last messages of a conversation instead of relying only on its own
store. The second is audit and support: operators can export what was asked and answered. The authoritative history
for the model remains the checkpointer or chat memory of the core (see
LangChain: Short-Term Memory & Checkpointers and
Spring AI: Chat Memory); the archive is a convenience, and it stores messages in
clear text on the server, so privacy policy and retention must be decided consciously. slixmpp exposes retrieve() and an
async generator iterate() on the xep_0313 plugin; Smack provides MamManager with queryArchive(MamQueryArgs), which
returns a MamQuery page and supports pageNext().
The XEPs used
| XEP | Title | Status (2026-10-01) | What the bot uses it for |
|---|---|---|---|
XEP-0030 |
Service Discovery |
Final |
Learning whether the server supports MAM, MUC and corrections; advertising the bot’s own features. |
XEP-0045 |
Multi-User Chat |
Stable |
Rooms: join with a nickname, receive |
XEP-0085 |
Chat State Notifications |
Final |
|
XEP-0114 |
Jabber Component Protocol |
Active, historical type |
The component mode: a sub-domain with a shared secret, used for gateways and multi-identity services. |
XEP-0184 |
Message Delivery Receipts |
Stable |
Optional; confirms that the user’s client received an answer. |
XEP-0198 |
Stream Management |
Stable |
Resumes a dropped TCP connection without losing stanzas; valuable for a long-running bot. |
XEP-0199 |
XMPP Ping |
Final |
Keep-alive and dead-connection detection. |
XEP-0280 |
Message Carbons |
Stable |
Lets the bot’s other resources (for example a second worker) see a copy of messages for a single account. |
XEP-0308 |
Last Message Correction |
Stable |
Pseudo-streaming: edit one message as tokens arrive, at a bounded rate. |
XEP-0313 |
Message Archive Management |
Stable |
Reading conversation history from the server for continuity and audit. |
The Python bot: slixmpp over the LangChain core
slixmpp is an asyncio library, which matches the async generator of the LangChain core. The bot registers the plugins it
needs by their xep_NNNN names, handles the session_start event to send presence, and handles the message event.
The plugin names used here are xep_0030 (discovery), xep_0085 (chat states), xep_0199 (ping) and xep_0308 (the
replace plugin, whose helper correct_message(msg, body) builds and sends a correction of an earlier message).
import asyncio
import os
import time
import slixmpp
from docs_assistant.core import ask # async generator of {"type": "token"|"citation"|"done", ...}
EDIT_INTERVAL = 1.5 # seconds between corrections
class DocsAssistantBot(slixmpp.ClientXMPP):
def __init__(self, jid: str, password: str):
super().__init__(jid, password)
self.register_plugin("xep_0030") # Service Discovery
self.register_plugin("xep_0085") # Chat state notifications
self.register_plugin("xep_0199") # XMPP Ping
self.register_plugin("xep_0308") # Last message correction
self.add_event_handler("session_start", self.on_start)
self.add_event_handler("message", self.on_message)
async def on_start(self, _event):
self.send_presence()
await self.get_roster()
def on_message(self, msg):
if msg["type"] not in ("chat", "normal") or not msg["body"].strip():
return # ignore groupchat here, chat-state-only stanzas and empty bodies
asyncio.create_task(self.answer(msg))
def send_state(self, to, state: str):
state_msg = self.make_message(mto=to, mtype="chat")
state_msg["chat_state"] = state
state_msg.send()
async def answer(self, incoming):
peer = incoming["from"]
user_id = peer.bare
conversation_id = f"{user_id}|{self.boundjid.bare}"
self.send_state(peer, "composing")
text, citations = "", []
first = None
last_edit = 0.0
async for event in ask(conversation_id, user_id, incoming["body"]):
if event["type"] == "token":
text += event["text"]
elif event["type"] == "citation":
citations.append(event)
now = time.monotonic()
if text and now - last_edit >= EDIT_INTERVAL:
if first is None:
first = self.make_message(mto=peer, mbody=text, mtype="chat")
first.send()
else:
self["xep_0308"].correct_message(first, text)
last_edit = now
final = text + "".join(f"\n[{i + 1}] {c['url']}" for i, c in enumerate(citations))
if first is None:
self.make_message(mto=peer, mbody=final, mtype="chat").send()
else:
self["xep_0308"].correct_message(first, final)
self.send_state(peer, "active")
async def main():
bot = DocsAssistantBot(os.environ["XMPP_JID"], os.environ["XMPP_PASSWORD"])
bot.connect()
await bot.disconnected
if __name__ == "__main__":
asyncio.run(main())
Source: https://slixmpp.readthedocs.io/en/latest/getting_started/echobot.html (client class, session_start and message
events), https://slixmpp.readthedocs.io/en/latest/api/plugins/xep_0308.html (correct_message) and
https://slixmpp.readthedocs.io/en/latest/api/plugins/xep_0085.html (the chat_state stanza interface).
Notes on the sketch. The password comes from the environment and is never written to source or logs. The handler returns
immediately and schedules the answer as a task so that one slow generation does not block the stream, which also keeps
pings and presence flowing. The ask async generator is the one from the LangChain core (create_agent, retriever tool
and Postgres checkpointer, streamed with astream; see
LangChain: Streaming); because the checkpointer is keyed by conversation_id, a
restart of the bot does not lose the thread. The first message needs an id for the correction to refer to; slixmpp
assigns one when the message is created, and correct_message uses it. Two details are version dependent and should be
checked against the installed slixmpp: whether bot.connect() needs an explicit address when the domain has SRV records,
and whether disconnected is the awaitable you want for shutdown handling.
Joining a room
For a room, use the XEP-0045 plugin (self.register_plugin("xep_0045")) and its asynchronous join:
async def on_start(self, _event):
self.send_presence()
await self["xep_0045"].join_muc_wait(
"docs@conference.chat.example.com", "DocsAssistant", maxstanzas=0, timeout=30
)
# in __init__:
# self.add_event_handler("groupchat_message", self.on_room_message)
def on_room_message(self, msg):
nick = "DocsAssistant"
if msg["mucnick"] == nick or nick.lower() not in msg["body"].lower():
return # ignore our own echoes and messages that do not address us
asyncio.create_task(self.answer_room(msg))
Source: https://slixmpp.readthedocs.io/en/latest/api/plugins/xep_0045.html (join_muc_wait, groupchat_message).
maxstanzas=0 asks the room not to replay history on join, so the bot does not answer old questions. In rooms the reply
is sent as mtype="groupchat" to msg["from"].bare; the correction logic is identical to the one-to-one case.
Reading history
async def recent(self, peer_bare_jid: str, limit: int = 20):
messages = []
async for result in self["xep_0313"].iterate(with_jid=peer_bare_jid, reverse=True, total=limit):
messages.append(result["mam_result"]["forwarded"]["stanza"]["body"])
return messages
Source: https://slixmpp.readthedocs.io/en/latest/api/plugins/xep_0313.html (retrieve and the iterate async generator).
This needs register_plugin("xep_0313") and a server with MAM enabled. The exact shape of the yielded result differs
between slixmpp versions; check the plugin’s reference before relying on that access path.
Running as a component
For component mode, replace the client class with slixmpp.ComponentXMPP and connect with the component JID, the secret
and the server’s component host and port (read from environment variables again); the handler code is the same, except
that the bot must set the from of every outgoing stanza explicitly because a component can speak as any JID under its
domain, and it has no roster or archive of its own.
The Java bot: Smack over the Spring AI core
Smack’s chat2 package is the maintained API for one-to-one chats: ChatManager.getInstanceFor(connection) returns the
manager, addIncomingListener registers an IncomingChatMessageListener, and the Chat object it hands to the
listener has send(CharSequence) and send(Message). Chat states come from ChatStateManager.getInstance(connection)
with setCurrentState(ChatState, Chat) and the ChatState enum, and corrections from
org.jivesoftware.smackx.message_correct.element.MessageCorrectExtension, built from the id of the original message.
The core is a Spring bean with a reactive signature that returns the events of the Spring AI ChatClient (RAG advisor, JDBC
chat memory, streamed with .stream(); see
Spring AI: Streaming & Reactive):
public interface AssistantCore {
Flux<AnswerEvent> ask(String conversationId, String userId, String text);
}
public record AnswerEvent(String type, String text, String url) {}
The adapter is a Spring component that owns the connection:
@Component
public class XmppAdapter {
private static final Duration EDIT_INTERVAL = Duration.ofMillis(1500);
private final AssistantCore core;
private final AbstractXMPPConnection connection;
public XmppAdapter(AssistantCore core,
@Value("${xmpp.domain}") String domain,
@Value("${xmpp.username}") String username,
@Value("${XMPP_PASSWORD}") String password) throws Exception {
this.core = core;
XMPPTCPConnectionConfiguration config = XMPPTCPConnectionConfiguration.builder()
.setXmppDomain(domain)
.setUsernameAndPassword(username, password)
.setSecurityMode(ConnectionConfiguration.SecurityMode.required)
.setResource("docs-assistant")
.build();
this.connection = new XMPPTCPConnection(config);
}
@PostConstruct
void start() throws Exception {
connection.connect().login();
ChatStateManager chatStates = ChatStateManager.getInstance(connection);
ChatManager.getInstanceFor(connection).addIncomingListener(
(EntityBareJid from, Message message, Chat chat) -> {
if (message.getBody() == null || message.getBody().isBlank()) {
return; // chat-state-only stanzas have no body
}
answer(chat, chatStates, from.toString(), message.getBody());
});
}
private void answer(Chat chat, ChatStateManager chatStates, String userId, String text) {
String conversationId = userId + "|" + connection.getUser().asBareJid();
send(() -> chatStates.setCurrentState(ChatState.composing, chat));
AtomicReference<String> firstId = new AtomicReference<>();
core.ask(conversationId, userId, text)
.scan(Draft.EMPTY, Draft::plus) // immutable cumulative state, no shared buffer
.skip(1) // drop the empty seed
.sample(EDIT_INTERVAL) // at most one edit per interval; the last draft is kept
.doOnNext(d -> publish(chat, firstId, d.text()))
.last(Draft.EMPTY)
.subscribeOn(Schedulers.boundedElastic())
.subscribe(
d -> {
publish(chat, firstId, d.withCitations());
send(() -> chatStates.setCurrentState(ChatState.active, chat));
},
error -> {
send(() -> chatStates.setCurrentState(ChatState.active, chat)); // clear "composing"
publish(chat, firstId, "Sorry, I could not finish this answer. Please try again.");
});
}
private record Draft(String text, List<String> citations) {
static final Draft EMPTY = new Draft("", List.of());
Draft plus(AnswerEvent e) {
if ("token".equals(e.type())) return new Draft(text + e.text(), citations);
if ("citation".equals(e.type())) {
List<String> next = new ArrayList<>(citations);
next.add(e.url());
return new Draft(text, List.copyOf(next));
}
return this;
}
String withCitations() {
StringBuilder full = new StringBuilder(text);
for (int i = 0; i < citations.size(); i++) {
full.append("\n[").append(i + 1).append("] ").append(citations.get(i));
}
return full.toString();
}
}
private void publish(Chat chat, AtomicReference<String> firstId, String body) {
send(() -> {
if (firstId.get() == null) {
String id = "docs-" + UUID.randomUUID();
firstId.set(id);
chat.send(StanzaBuilder.buildMessage(id)
.to(chat.getXmppAddressOfChatPartner())
.ofType(Message.Type.chat)
.setBody(body)
.build());
} else {
chat.send(StanzaBuilder.buildMessage()
.to(chat.getXmppAddressOfChatPartner())
.ofType(Message.Type.chat)
.setBody(body)
.addExtension(new MessageCorrectExtension(firstId.get()))
.build());
}
});
}
private void send(ThrowingRunnable action) {
try {
action.run();
} catch (Exception e) {
// log and drop: a failed typing indicator or edit must not break the stream
}
}
@FunctionalInterface
interface ThrowingRunnable { void run() throws Exception; }
@PreDestroy
void stop() {
connection.disconnect();
}
}
Source: https://www.igniterealtime.org/projects/smack/ and the Smack Javadoc: https://download.igniterealtime.org/smack/docs/latest/javadoc/org/jivesoftware/smack/chat2/ChatManager.html, https://download.igniterealtime.org/smack/docs/latest/javadoc/org/jivesoftware/smackx/chatstates/ChatStateManager.html and https://download.igniterealtime.org/smack/docs/latest/javadoc/org/jivesoftware/smackx/message_correct/element/MessageCorrectExtension.html.
Notes on the sketch. The password is injected from an environment variable. SecurityMode.required makes the connection
fail instead of falling back to plain text if the server does not offer TLS. Smack invokes the incoming listener on its own
processing thread, so the adapter hands the work to a Reactor scheduler instead of blocking on the stream, and sample
implements the rate limit by keeping only the latest state per interval. That state is built with scan into
immutable Draft values rather than a shared StringBuilder, because sample publishes from its timer thread while
events arrive on another; since each draft is cumulative, the dropped ones lose nothing, and last hands the final draft
to the subscriber, which adds the citations. The error consumer clears the composing state and replaces the partial
answer with an apology, so a failed stream never leaves the user watching a typing indicator. Two points are not confirmed from the documentation consulted: the stanza builder entry point
(StanzaBuilder.buildMessage(…) in Smack 4.5; the MessageBuilder class documents setBody, ofType and the inherited
to and addExtension) and the generation of a unique stanza id, which here is a plain UUID string and could simply be
StanzaIdUtil.newStanzaId(); verify both against the Smack 4.5.0 Javadoc. Smack 4.6.0-alpha1 exists but is not used.
Note also that Smack’s chat state manager only sends a state when it changes, which is why active at the end of the
answer is sent after composing and not repeated.
Rooms and history in Smack
For rooms, obtain MultiUserChatManager.getInstanceFor(connection), call getMultiUserChat(roomJid), then
join(Resourcepart nickname) (an overload accepts a MucEnterConfiguration to limit replayed history), register a
MessageListener with addMessageListener, and leave the room on shutdown with leave(). Smack’s MamManager, obtained
with MamManager.getInstanceFor(connection), offers isSupported() and queryArchive(MamQueryArgs).
Security
| Concern | Practice |
|---|---|
Transport |
Require TLS on the client-to-server connection (STARTTLS on port 5222 or direct TLS on 5223, depending on the server)
and verify the server certificate against the domain. Never disable certificate or hostname verification to make a
self-signed development server work in production code; add the development CA to the trust store instead. Smack’s
|
Authentication |
SASL: prefer SCRAM-SHA-256 or SCRAM-SHA-1 over PLAIN, and use PLAIN only inside a TLS session. A bot account should have a long random password stored in a secret manager (see Azure Key Vault for one option) and injected through the environment; components use a separate shared secret that is also a credential. |
Authorization |
Decide who may talk to the bot: a server-side allow-list, a restricted registration policy or roster-based filtering. Open federation lets any server on the internet send messages to the bot, which means unauthenticated senders from other domains; for an internal assistant, disable server-to-server connections or restrict them to trusted domains. |
End-to-end encryption |
TLS protects only the hops. The server, and the message archive, see clear text. If the assistant answers confidential questions, state that clearly; OMEMO-style end-to-end encryption is a separate set of specifications and clients and is not covered here. |
Abuse and cost |
Apply per-user rate limits and a maximum message length before calling the model, and limit concurrent generations per user. See Operating Chat Channels for the cross-channel view and Spring AI: Security & Guardrails for prompt-injection controls. |
Logging |
Do not log message bodies or stanza dumps in production; they contain user questions and retrieved text. |
When to choose XMPP
Choose it when you control the server, want federation or self-hosting, or your users already run XMPP clients. Do not choose it for a consumer product that needs cards, buttons or file previews, since XMPP clients render only text and whatever extensions they happen to support. Among the channels in this sub-section it ranks lowest for presentation and highest for control, and its adapter is correspondingly small. For web users the better path is Streaming Chat APIs or a web chat widget.
References
-
RFC 6120, XMPP: Core: https://datatracker.ietf.org/doc/html/rfc6120
-
RFC 6121, XMPP: Instant Messaging and Presence: https://datatracker.ietf.org/doc/html/rfc6121
-
XMPP Extension Protocols index: https://xmpp.org/extensions/
-
XEP-0045 Multi-User Chat: https://xmpp.org/extensions/xep-0045.html
-
XEP-0085 Chat State Notifications: https://xmpp.org/extensions/xep-0085.html
-
XEP-0114 Jabber Component Protocol: https://xmpp.org/extensions/xep-0114.html
-
XEP-0308 Last Message Correction: https://xmpp.org/extensions/xep-0308.html
-
XEP-0313 Message Archive Management: https://xmpp.org/extensions/xep-0313.html
-
ejabberd documentation: https://docs.ejabberd.im/
-
Prosody documentation: https://prosody.im/doc
-
slixmpp documentation: https://slixmpp.readthedocs.io/
-
Smack Javadoc: https://download.igniterealtime.org/smack/docs/latest/javadoc/