From 31f1de20f5251ebf09c1ec6f677dbc891b84808b Mon Sep 17 00:00:00 2001 From: Guus der Kinderen Date: Fri, 25 Sep 2026 20:17:45 +0200 Subject: [PATCH 1/4] fixes #267: Generate readme.html from readme.md during the build The HTML readme used to be regenerated manually from the markdown readme, which was often forgotten. It is now generated during the Maven build, and no longer tracked in git. The HTML uses inline styling, as the Openfire admin console's Content-Security-Policy blocks the external stylesheet that the previous version used. A small inline script builds the table of contents and makes heading IDs unique in the same way that GitHub does. Co-Authored-By: Claude Opus 5.5 --- .gitignore | 3 + pom.xml | 50 + readme.html | 2851 ---------------------------------------- src/readme/footer.html | 70 + src/readme/header.html | 95 ++ 5 files changed, 218 insertions(+), 2851 deletions(-) delete mode 100644 readme.html create mode 100644 src/readme/footer.html create mode 100644 src/readme/header.html diff --git a/.gitignore b/.gitignore index 797bbefa1..c81331225 100644 --- a/.gitignore +++ b/.gitignore @@ -33,3 +33,6 @@ debian/ /AGENTS.md /CLAUDE.md /.claude/settings.local.json + +# Generated from readme.md during the build +/readme.html diff --git a/pom.xml b/pom.xml index 067c625f4..e562316f1 100644 --- a/pom.xml +++ b/pom.xml @@ -46,6 +46,56 @@ maven-surefire-plugin 3.6.0 + + + + + maven-resources-plugin + + + copy-readme-markdown + generate-sources + + copy-resources + + + ${project.build.directory}/readme + + + ${project.basedir} + + readme.md + + + + + + + + + + com.ruleoftech + markdown-page-generator-plugin + 2.5.2 + + + generate-readme-html + generate-resources + + generate + + + + + ${project.build.directory}/readme + ${project.basedir} + ${project.basedir}/src/readme/header.html + ${project.basedir}/src/readme/footer.html + Openfire REST API Plugin Readme + true + TABLES,FENCED_CODE_BLOCKS,AUTOLINKS,STRIKETHROUGH,ANCHORLINKS + + diff --git a/readme.html b/readme.html deleted file mode 100644 index 054238b27..000000000 --- a/readme.html +++ /dev/null @@ -1,2851 +0,0 @@ - - - - - - - Openfire REST API Plugin Readme - - - - -
-
- - - -
-
-
-
-

REST API Plugin Readme

-

The REST API Plugin provides the ability to manage Openfire by sending an REST/HTTP request to the server. This plugin’s functionality is useful for applications that need to administer Openfire outside of the Openfire admin console.

-

CI Build Status

-

Build Status

-

Reporting Issues

-

Issues may be reported to the forums or via this repo’s Github Issues.

-

Feature list

-
    -
  • Get overview over all or specific user and to create, update or delete a user
  • -
  • Get overview over all or specific group and to create, update or delete a group
  • -
  • Get overview over all user roster entries and to add, update or delete a roster entry
  • -
  • Add user to a group and remove a user from a group
  • -
  • Lockout, unlock or kick the user (enable / disable)
  • -
  • Get overview over all or specific system properties and to create, update or delete system property
  • -
  • Get overview over all or specific chat room and to create, update or delete a chat room
  • -
  • Get overview over all or specific user sessions
  • -
  • Send broadcast message to all online users
  • -
  • Get overview of all or specific security audit logs
  • -
  • Get chat message history from a multi user chat room
  • -
  • Get clustering status of Openfire
  • -
  • Get overview of ‘readiness’ and ‘liveness’ state of Openfire
  • -
-

Available REST API clients

-

REST API clients are implementations of the REST API in a specific programming language.

-

Official

- -

Third party

- -

Installation

-

Copy restAPI.jar into the plugins directory of your Openfire server. The plugin will be automatically deployed. To upgrade to a newer version, overwrite the restAPI.jar file with the new one.

-

Important Step: To enable the plugin make sure to set the system property adminConsole.access.allow-wildcards-in-excludes to true

-

Without the above step the REST API plugin always redirects to login.
- This was done in response to a security issue.

-

Explanation of REST

-

To provide a standard way of accessing the data the plugin is using REST.

- - - - - - - - - - - - - - - - - - - - - - - - - - -
HTTP MethodUsage
GETReceive a read-only data
PUTOverwrite an existing resource
POSTCreates a new resource
DELETEDeletes the given resource

Authentication

-

All REST Endpoint are secured and must be authenticated. There are two ways to authenticate:

- -

The configuration can be done in Openfire Admin console under Server > Server Settings > REST API.

-

Basic HTTP Authentication

-

To access the endpoints is that required to send the Username and Password of a Openfire Admin account in your HTTP header request.

-

E.g., for username: admin and password: 12345:

-
-

Header: Authorization: Basic YWRtaW46MTIzNDU=

-
-

Shared secret key

-

To access the endpoints is that required to send the secret key in your header request.
- The secret key can be defined in Openfire Admin console under Server > Server Settings > REST API.

-

E.g.

-
-

Header: Authorization: s3cretKey

-
-

User related REST Endpoints

-

Retrieve users

-

Endpoint to get all or filtered users

-
-

GET /users

-
-

Payload: none

-

Return value: Users

-

Possible parameters

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
ParameterParameter TypeDescriptionDefault value
search@QueryParamSearch/Filter by username.
This act like the wildcard search %String%
propertyKey@QueryParamFilter by user propertyKey.
propertyValue@QueryParamFilter by user propertyKey and propertyValue.
Note: It can only be used within propertyKey parameter

Examples

-
-

Header: Authorization: Basic YWRtaW46MTIzNDU=

-
-
-

GET http://example.org:9090/plugins/restapi/v1/users

-
-
-

GET http://example.org:9090/plugins/restapi/v1/users?search=testuser

-
-
-

GET http://example.org:9090/plugins/restapi/v1/users?propertyKey=keyname

-
-
-

GET http://example.org:9090/plugins/restapi/v1/users?propertyKey=keyname&propertyValue=keyvalue

-
-

If you want to get a JSON format result, please add “Accept: application/json” to the Header.

-

Retrieve a user

-

Endpoint to get information over a specific user

-
-

GET /users/{username}

-
-

Payload: none

-

Return value: User

-

Possible parameters

- - - - - - - - - - - - - - - - - - -
ParameterParameter TypeDescriptionDefault value
username@PathExact username

Examples

-
-

Header: Authorization: Basic YWRtaW46MTIzNDU=

-

GET http://example.org:9090/plugins/restapi/v1/users/testuser

-
-

Create a user

-

Endpoint to create a new user

-
-

POST /users

-
-

Payload: User
- Return value: HTTP status 201 (Created)

-

Examples

-

XML Examples

-
-

Header: Authorization: Basic YWRtaW46MTIzNDU=

-

Header: Content-Type: application/xml

-

POST http://example.org:9090/plugins/restapi/v1/users

-
-

Payload Example 1 (required parameters):

-
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
-<user>  
- <username>test3</username> <password>p4ssword</password></user>  
-
-

Payload Example 2 (available parameters):

-
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
-<user>  
- <username>testuser</username> <password>p4ssword</password> <name>Test User</name> <email>test@localhost.de</email> <properties> <property key="keyname" value="value"/> <property key="anotherkey" value="value"/> </properties></user>  
-
-

JSON Examples

-
-

Header: Authorization: Basic YWRtaW46MTIzNDU=

-

Header: Content-Type: application/json

-

POST http://example.org:9090/plugins/restapi/v1/users

-
-

Payload Example 1 (required parameters):

-
{
- "username": "admin", "password": "p4ssword"}  
-
-

Payload Example 2 (available parameters):

-
{
- "username": "admin", "password": "p4ssword", "name": "Administrator", "email": "admin@example.com", "properties": { "property": [ { "@key": "console.rows_per_page", "@value": "user-summary=8" }, { "@key": "console.order", "@value": "session-summary=1" } ] }}  
-
-

REST API Version 1.3.0 and later - Payload Example 3 (available parameters):

-
{
- "users": [ { "username": "admin", "name": "Administrator", "email": "admin@example.com", "password": "p4ssword", "properties": [ { "key": "console.order", "value": "session-summary=0" } ] }, { "username": "test", "name": "Test", "password": "p4ssword" } ]}  
-
-

Delete a user

-

Endpoint to delete a user

-
-

DELETE /users/{username}

-
-

Payload: none

-

Return value: HTTP status 200 (OK)

-

Possible parameters

- - - - - - - - - - - - - - - - - - -
ParameterParameter TypeDescriptionDefault value
username@PathExact username

Examples

-
-

Header: Authorization: Basic YWRtaW46MTIzNDU=

-

DELETE http://example.org:9090/plugins/restapi/v1/users/testuser

-
-

Update a user

-

Endpoint to update / rename a user

-
-

PUT /users/{username}

-
-

Payload: User

-

Return value: HTTP status 200 (OK)

-

Possible parameters

- - - - - - - - - - - - - - - - - - -
ParameterParameter TypeDescriptionDefault value
username@PathExact username

Examples

-

XML Example

-
-

Header: Authorization: Basic YWRtaW46MTIzNDU=

-

Header: Content-Type application/xml

-

PUT http://example.org:9090/plugins/restapi/v1/users/testuser

-
-

Payload:

-
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
-<user>  
- <username>testuser</username> <name>Test User edit</name> <email>test@edit.de</email> <properties> <property key="keyname" value="value"/> </properties></user>  
-
-

Rename Example

-
-

Header: Authorization: Basic YWRtaW46MTIzNDU=

-

Header: Content-Type application/xml

-

PUT http://example.org:9090/plugins/restapi/v1/users/oldUsername

-
-

Payload:

-
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
-<user>  
- <username>newUsername</username> <name>Test User edit</name> <email>test@edit.de</email> <properties> <property key="keyname" value="value"/> </properties></user>  
-
-

JSON Example

-
-

Header: Authorization: Basic YWRtaW46MTIzNDU=

-

Header: Content-Type application/json

-

PUT http://example.org:9090/plugins/restapi/v1/users/testuser

-
-

Payload:

-
{
- "username": "testuser", "name": "Test User edit", "email": "test@edit.de", "properties": { "property": { "@key": "keyname", "@value": "value" } }}  
-
-

REST API Version 1.3.0 and later - Payload Example 2 (available parameters):

-
{
- "username": "testuser", "name": "Test User edit", "email": "test@edit.de", "properties": [ { "key": "keyname", "value": "value" } ]}  
-
-

Retrieve all user groups

-

Endpoint to get group names of a specific user

-
-

GET /users/{username}/groups

-
-

Payload: none

-

Return value: Groups

-

Possible parameters

- - - - - - - - - - - - - - - - - - -
ParameterParameter TypeDescriptionDefault value
username@PathExact username

Examples

-
-

Header: Authorization: Basic YWRtaW46MTIzNDU=

-

GET http://example.org:9090/plugins/restapi/v1/users/testuser/groups

-
-

Add user to groups

-

Endpoint to add user to a groups

-
-

POST /users/{username}/groups

-
-

Payload: Groups

-

Return value: HTTP status 201 (Created)

-

Possible parameters

- - - - - - - - - - - - - - - - - - -
ParameterParameter TypeDescriptionDefault value
username@PathExact username

Examples

-
-

Header: Authorization: Basic YWRtaW46MTIzNDU=

-

Header: Content-Type application/xml

-

POST http://example.org:9090/plugins/restapi/v1/users/testuser/groups

-
-

Payload:

-
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
-<groups>  
- <groupname>Admins</groupname> <groupname>Support</groupname></groups>  
-
-

Add user to group

-

Endpoint to add user to a group

-
-

POST /users/{username}/groups/{groupName}

-
-

Payload: none

-

Return value: HTTP status 201 (Created)

-

Possible parameters

- - - - - - - - - - - - - - - - - - - - - - - - -
ParameterParameter TypeDescriptionDefault value
username@PathExact username
groupName@PathExact group name

Examples

-
-

Header: Authorization: Basic YWRtaW46MTIzNDU=

-

Header: Content-Type application/xml

-

POST http://example.org:9090/plugins/restapi/v1/users/testuser/groups/testGroup

-
-

Delete a user from a groups

-

Endpoint to remove a user from a groups

-
-

DELETE /users/{username}/groups

-
-

Payload: Groups

-

Return value: HTTP status 200 (OK)

-

Possible parameters

- - - - - - - - - - - - - - - - - - -
ParameterParameter TypeDescriptionDefault value
username@PathExact username

Examples

-
-

Header: Authorization: Basic YWRtaW46MTIzNDU=

-

Header: Content-Type application/xml

-

DELETE http://example.org:9090/plugins/restapi/v1/users/testuser/groups

-
-

Payload:

-
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
-<groups>  
- <groupname>Admins</groupname> <groupname>Support</groupname></groups>  
-
-

Delete a user from a group

-

Endpoint to remove a user from a group

-
-

DELETE /users/{username}/groups/{groupName}

-
-

Payload: none

-

Return value: HTTP status 200 (OK)

-

Possible parameters

- - - - - - - - - - - - - - - - - - - - - - - - -
ParameterParameter TypeDescriptionDefault value
username@PathExact username
groupName@PathExact group name

Examples

-
-

Header: Authorization: Basic YWRtaW46MTIzNDU=

-

Header: Content-Type application/xml

-

DELETE http://example.org:9090/plugins/restapi/v1/users/testuser/groups/testGroup

-
-

Lockout a user

-

Endpoint to lockout / ban the user from the chat server. The user will be kicked if the user is online.

-
-

POST /lockouts/{username}

-
-

Payload: none

-

Return value: HTTP status 201 (Created)

-

Possible parameters

- - - - - - - - - - - - - - - - - - -
ParameterParameter TypeDescriptionDefault value
username@PathExact username

Examples

-
-

Header: Authorization: Basic YWRtaW46MTIzNDU=

-

POST http://example.org:9090/plugins/restapi/v1/lockouts/testuser

-
-

Unlock a user

-

Endpoint to unlock / unban the user

-
-

DELETE /lockouts/{username}

-
-

Payload: none

-

Return value: HTTP status 200 (OK)

-

Possible parameters

- - - - - - - - - - - - - - - - - - -
ParameterParameter TypeDescriptionDefault value
username@PathExact username

Examples

-
-

Header: Authorization: Basic YWRtaW46MTIzNDU=

-

DELETE http://example.org:9090/plugins/restapi/v1/lockouts/testuser

-
-

Retrieve user roster

-

Endpoint to get roster entries (buddies) from a specific user

-
-

GET /users/{username}/roster

-
-

Payload: none

-

Return value: Roster

-

Possible parameters

- - - - - - - - - - - - - - - - - - -
ParameterParameter TypeDescriptionDefault value
username@PathExact username

Examples

-
-

Header: Authorization: Basic YWRtaW46MTIzNDU=

-

GET http://example.org:9090/plugins/restapi/v1/users/testuser/roster

-
-

Create a user roster entry

-

Endpoint to add a new roster entry to a user

-
-

POST /users/{username}/roster

-
-

Payload: RosterItem

-

Return value: HTTP status 201 (Created)

-

Possible parameters

- - - - - - - - - - - - - - - - - - -
ParameterParameter TypeDescriptionDefault value
username@PathExact username

Examples

-
-

Header: Authorization: Basic YWRtaW46MTIzNDU=

-

Header: Content-Type application/xml

-

POST http://example.org:9090/plugins/restapi/v1/users/testuser/roster

-
-

Payload:
- Payload Example 1 (required parameters):

-
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
-<rosterItem>  
- <jid>peter@pan.de</jid></rosterItem>  
-
-

Payload Example 2 (available parameters):

-
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
-<rosterItem>  
- <jid>peter@pan1.de</jid> <nickname>Peter1</nickname> <subscriptionType>3</subscriptionType> <groups> <group>Friends</group> </groups></rosterItem>  
-
-

Delete a user roster entry

-

Endpoint to remove a roster entry from a user

-
-

DELETE /users/{username}/roster/{jid}

-
-

Payload: none

-

Return value: HTTP status 200 (OK)

-

Possible parameters

- - - - - - - - - - - - - - - - - - - - - - - - -
ParameterParameter TypeDescriptionDefault value
username@PathExact username
jid@PathJID of the roster item

Examples

-
-

Header: Authorization: Basic YWRtaW46MTIzNDU=

-

DELETE http://example.org:9090/plugins/restapi/v1/users/testuser/roster/peter@pan.de

-
-

Update a user roster entry

-

Endpoint to update a roster entry

-
-

PUT /users/{username}/roster/{jid}

-
-

Payload: RosterItem

-

Return value: HTTP status 200 (OK)

-

Possible parameters

- - - - - - - - - - - - - - - - - - - - - - - - -
ParameterParameter TypeDescriptionDefault value
username@PathExact username
jid@PathJID of the roster item

Examples

-
-

Header: Authorization: Basic YWRtaW46MTIzNDU=

-

Header: Content-Type application/xml

-

PUT http://example.org:9090/plugins/restapi/v1/users/testuser/roster/peter@pan.de

-
-

Payload:

-
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
-<rosterItem>  
- <jid>peter@pan.de</jid> <nickname>Peter Pan</nickname> <subscriptionType>0</subscriptionType> <groups> <group>Support</group> </groups></rosterItem>  
-
-

Retrieve user’s vcard

-

Endpoint to get the vCard of a particular user

-
-

GET /users/{username}/vcard

-
-

Payload: none

-

Return value: vCard XML data

-

Possible parameters

- - - - - - - - - - - - - - - - - - -
ParameterParameter TypeDescriptionDefault value
username@PathExact username

Examples

-
-

Header: Authorization: Basic YWRtaW46MTIzNDU=

-

GET http://example.org:9090/plugins/restapi/v1/users/testuser/vcard

-
-

Add or update user’s vCard

-

Endpoint to add or replace a vCard of a particular user.

-
-

PUT /users/{username}/vcard

-
-

Payload: vCard XML data

-

Return value: HTTP status 200 (Created)

-

Possible parameters

- - - - - - - - - - - - - - - - - - -
ParameterParameter TypeDescriptionDefault value
username@PathExact username

Examples

-
-

Header: Authorization: Basic YWRtaW46MTIzNDU=

-

Header: Content-Type application/xml

-

POST http://example.org:9090/plugins/restapi/v1/users/testuser/vcard

-
-

Payload:

-
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
-<vCard xmlns="vcard-temp">  
- <N> <FAMILY>Doe</FAMILY> <GIVEN>Janice</GIVEN> <MIDDLE>Francis</MIDDLE> </N> <ORG> <ORGNAME/> <ORGUNIT/> </ORG> <NICKNAME>Jane</NICKNAME> <FN>Janice Francis Doe</FN> <TITLE/> <URL/> <EMAIL> <HOME/> <INTERNET/> <PREF/> <USERID>j.doe@example.org</USERID> </EMAIL> <TEL> <WORK/> <VOICE/> <NUMBER/> </TEL> <TEL> <WORK/> <PAGER/> <NUMBER/> </TEL> <TEL> <WORK/> <FAX/> <NUMBER/> </TEL> <TEL> <WORK/> <CELL/> <NUMBER/> </TEL> <TEL> <HOME/> <VOICE/> <NUMBER/> </TEL> <TEL> <HOME/> <PAGER/> <NUMBER/> </TEL> <TEL> <HOME/> <FAX/> <NUMBER/> </TEL> <TEL> <HOME/> <CELL/> <NUMBER/> </TEL> <ADR> <WORK/> <LOCALITY/> <CTRY/> <STREET/> <PCODE/> <REGION/> </ADR> <ADR> <HOME/> <LOCALITY/> <CTRY/> <STREET/> <PCODE/> <REGION/> </ADR></vCard>  
-
-

Delete user’s vcard

-

Endpoint to remove the vCard of a particular user

-
-

DELETE /users/{username}/vcard

-
-

Payload: none

-

Return value: none

-

Possible parameters

- - - - - - - - - - - - - - - - - - -
ParameterParameter TypeDescriptionDefault value
username@PathExact username

Examples

-
-

Header: Authorization: Basic YWRtaW46MTIzNDU=

-

DELETE http://example.org:9090/plugins/restapi/v1/users/testuser/vcard

-
-

Chat room related REST Endpoints

-

Retrieve all chat services

-

Endpoint to get all chat services

-
-

GET /chatservices

-
-

Payload: none

-

Return value: Chat services

-

Possible parameters: none

-

Examples

-
-

Header: Authorization: Basic YWRtaW46MTIzNDU=

-

GET http://example.org:9090/plugins/restapi/v1/chatservices

-
-

Create a chat service

-

Endpoint to create a new chat service.

-
-

POST /chatservices

-
-

Payload: Chatservice

-

Return value: HTTP status 201 (Created)

-

Possible parameters: none

-

XML Examples

-
-

Header: Authorization: Basic YWRtaW46MTIzNDU=

-

Header: Content-Type: application/xml

-

POST http://example.org:9090/plugins/restapi/v1/chatservices

-
-

Payload Example (available parameters):

-
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
-<chatService>  
- <serviceName>new-chat-service-name</serviceName> <description>A mightily fine service</description> <hidden>false</hidden></chatService>  
-
-

JSON Examples

-
-

Header: Authorization: Basic YWRtaW46MTIzNDU=

-

Header: Content-Type: application/json

-

POST http://example.org:9090/plugins/restapi/v1/chatservices

-
-

Payload Example (available parameters):

-
{
- "serviceName": "new-chat-service-name", "description": "A mightily fine service", "hidden": false}  
-
-

Retrieve all chat rooms

-

Endpoint to get all chat rooms

-
-

GET /chatrooms

-
-

Payload: none

-

Return value: Chatrooms

-

Possible parameters

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
ParameterParameter TypeDescriptionDefault value
servicename@QueryParamThe name of the Group Chat Serviceconference
type@QueryParampublic: Only as List Room in Directory set rooms
all: All rooms.
public
search@QueryParamSearch/Filter by room name.
This act like the wildcard search %String%

Examples

-
-

Header: Authorization: Basic YWRtaW46MTIzNDU=

-

GET http://example.org:9090/plugins/restapi/v1/chatrooms

-

GET http://example.org:9090/plugins/restapi/v1/chatrooms?type=all

-

GET http://example.org:9090/plugins/restapi/v1/chatrooms?type=all&servicename=privateconf

-

GET http://example.org:9090/plugins/restapi/v1/chatrooms?search=test

-
-

Retrieve a chat room

-

Endpoint to get information over specific chat room

-
-

GET /chatrooms/{roomName}

-
-

Payload: none

-

Return value: Chatroom

-

Possible parameters

- - - - - - - - - - - - - - - - - - - - - - - - -
ParameterParameter TypeDescriptionDefault value
roomname@PathExact room name
servicename@QueryParamThe name of the Group Chat Serviceconference

Examples

-
-

Header: Authorization: Basic YWRtaW46MTIzNDU=

-

GET http://example.org:9090/plugins/restapi/v1/chatrooms/test

-

GET http://example.org:9090/plugins/restapi/v1/chatrooms/test?servicename=privateconf

-
-

Retrieve chat room participants

-

Endpoint to get all participants with a role of specified room.

-
-

GET /chatrooms/{roomName}/participants

-
-

Payload: none

-

Return value: Participants

-

Possible parameters

- - - - - - - - - - - - - - - - - - - - - - - - -
ParameterParameter TypeDescriptionDefault value
roomname@PathExact room name
servicename@QueryParamThe name of the Group Chat Serviceconference

Examples

-
-

Header: Authorization: Basic YWRtaW46MTIzNDU=

-

GET http://example.org:9090/plugins/restapi/v1/chatrooms/room1/participants

-
-

Retrieve chat room occupants

-

Endpoint to get all occupants (all roles / affiliations) of a specified room.

-
-

GET /chatrooms/{roomName}/occupants

-
-

Payload: none

-

Return value: Occupants

-

Possible parameters

- - - - - - - - - - - - - - - - - - - - - - - - -
ParameterParameter TypeDescriptionDefault value
roomname@PathExact room name
servicename@QueryParamThe name of the Group Chat Serviceconference

Examples

-
-

Header: Authorization: Basic YWRtaW46MTIzNDU=

-

GET http://example.org:9090/plugins/restapi/v1/chatrooms/room1/occupants

-
-

Retrieve chat room message history

-

Endpoint to get the chat message history of a specified room.

-
-

GET /chatrooms/{roomName}/chathistory

-
-

Payload: none

-

Return value: Chat History

-

Possible parameters

- - - - - - - - - - - - - - - - - - - - - - - - -
ParameterParameter TypeDescriptionDefault value
roomname@PathExact room name
servicename@QueryParamThe name of the Group Chat Serviceconference

Create a chat room

-

Endpoint to create a new chat room.

-
-

POST /chatrooms

-
-

Payload: Chatroom

-

Return value: HTTP status 201 (Created)

-

Possible parameters

- - - - - - - - - - - - - - - - - - - - - - - - -
ParameterParameter TypeDescriptionDefault value
servicename@QueryParamThe name of the Group Chat Serviceconference
sendInvitations@QueryParamWhether to send invitations to affiliated usersfalse

XML Examples

-
-

Header: Authorization: Basic YWRtaW46MTIzNDU=

-

Header: Content-Type: application/xml

-

POST http://example.org:9090/plugins/restapi/v1/chatrooms

-
-

Payload Example 1 (required parameters):

-
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
-<chatRoom>  
- <naturalName>global-1</naturalName> <roomName>global</roomName> <description>Global Chat Room</description></chatRoom>  
-
-

Payload Example 2 (available parameters):

-
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
-<chatRoom>  
- <roomName>global</roomName> <naturalName>global-2</naturalName> <description>Global Chat Room</description> <subject>global-2 Subject</subject> <creationDate>2014-02-12T15:52:37.592+01:00</creationDate> <modificationDate>2014-09-12T15:35:54.702+02:00</modificationDate> <maxUsers>0</maxUsers> <persistent>true</persistent> <publicRoom>true</publicRoom> <registrationEnabled>false</registrationEnabled> <canAnyoneDiscoverJID>false</canAnyoneDiscoverJID> <canOccupantsChangeSubject>false</canOccupantsChangeSubject> <canOccupantsInvite>false</canOccupantsInvite> <canChangeNickname>false</canChangeNickname> <logEnabled>true</logEnabled> <loginRestrictedToNickname>false</loginRestrictedToNickname> <membersOnly>false</membersOnly> <moderated>false</moderated> <allowPM>anyone</allowPM> <broadcastPresenceRoles> <broadcastPresenceRole>moderator</broadcastPresenceRole> <broadcastPresenceRole>participant</broadcastPresenceRole> <broadcastPresenceRole>visitor</broadcastPresenceRole> </broadcastPresenceRoles> <owners> <owner>owner@localhost</owner> </owners> <admins> <admin>admin@localhost</admin> </admins> <members> <member>member2@localhost</member> <member>member1@localhost</member> </members> <outcasts> <outcast>outcast1@localhost</outcast> </outcasts></chatRoom>  
-
-

JSON Examples

-
-

Header: Authorization: Basic YWRtaW46MTIzNDU=

-

Header: Content-Type: application/json

-

POST http://example.org:9090/plugins/restapi/v1/chatrooms

-
-

Payload Example 1 (required parameters):

-
{
- "roomName": "global", "naturalName": "global-2", "description": "Global chat room"}  
-
-

Payload Example 2 (available parameters):

-
{
- "roomName": "global-1", "naturalName": "global-1_test_hello", "description": "Global chat room", "subject": "Global chat room subject", "creationDate": "2012-10-18T16:55:12.803+02:00", "modificationDate": "2014-07-10T09:49:12.411+02:00", "maxUsers": "0", "persistent": "true", "publicRoom": "true", "registrationEnabled": "false", "canAnyoneDiscoverJID": "true", "canOccupantsChangeSubject": "false", "canOccupantsInvite": "false", "canChangeNickname": "false", "logEnabled": "true", "loginRestrictedToNickname": "true", "membersOnly": "false", "moderated": "false", "allowPM": "anyone", "broadcastPresenceRoles": { "broadcastPresenceRole": [ "moderator", "participant", "visitor" ] }, "owners": { "owner": "owner@localhost" }, "admins": { "admin": [ "admin@localhost", "admin2@localhost" ] }, "members": { "member": [ "member@localhost", "member2@localhost" ] }, "outcasts": { "outcast": [ "outcast@localhost", "outcast2@localhost" ] }}  
-
-

REST API Version 1.3.0 and later - Payload Example 2 (available parameters):

-
{
- "roomName": "global-1", "naturalName": "global-1_test_hello", "description": "Global chat room", "subject": "Global chat room subject", "creationDate": "2012-10-18T16:55:12.803+02:00", "modificationDate": "2014-07-10T09:49:12.411+02:00", "maxUsers": "0", "persistent": "true", "publicRoom": "true", "registrationEnabled": "false", "canAnyoneDiscoverJID": "true", "canOccupantsChangeSubject": "false", "canOccupantsInvite": "false", "canChangeNickname": "false", "logEnabled": "true", "loginRestrictedToNickname": "true", "membersOnly": "false", "moderated": "false", "allowPM": "anyone", "broadcastPresenceRoles": [ "moderator", "participant", "visitor" ], "owners": [ "owner@localhost" ], "admins": [ "admin@localhost" ], "members": [ "member@localhost" ], "outcasts": [ "outcast@localhost" ]}  
-
-

Create multiple chat room

-

Endpoint to create multiple new chat rooms at once.

-
-

POST /chatrooms/bulk

-
-

Payload: Chatrooms

-

Return value: Result list, ordered by successes and failures

-
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
-<results>  
- <success> <result> <roomName>room1</roomName> <resultType>Success</resultType> <message>Room was successfully created</message> </result> <result> <roomName>room2</roomName> <resultType>Success</resultType> <message>Room was successfully created</message> </result> </success> <failure/> <other/></results>  
-
-
{
- "success": [ { "roomName": "room1", "resultType": "Success", "message": "Room was successfully created" }, { "roomName": "room2", "resultType": "Success", "message": "Room was successfully created" } ], "failure": [], "other": []}  
-
-

Possible parameters

- - - - - - - - - - - - - - - - - - - - - - - - -
ParameterParameter TypeDescriptionDefault value
servicename@QueryParamThe name of the Group Chat Serviceconference
sendInvitations@QueryParamWhether to send invitations to newly affiliated usersfalse

XML Examples

-
-

Header: Authorization: Basic YWRtaW46MTIzNDU=

-

Header: Content-Type: application/xml

-

POST http://example.org:9090/plugins/restapi/v1/chatrooms/bulk

-
-

Payload Example:

-
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
-<chatRooms>  
- <chatRoom> <roomName>room1</roomName> <description>description1</description> </chatRoom> <chatRoom> <roomName>room2</roomName> <description>description1</description> </chatRoom></chatRooms>  
-
-

For more examples, with more parameters, see the create a chat room endpoint.

-

JSON Examples

-
-

Header: Authorization: Basic YWRtaW46MTIzNDU=

-

Header: Content-Type: application/json

-

POST http://example.org:9090/plugins/restapi/v1/chatrooms

-
-

Payload Example 1 (required parameters):

-
{
- "chatRooms": [ { "roomName": "room1", "description": "description1" }, { "roomName": "room2", "description": "description2" } ]}  
-
-

For more examples, with more parameters, see the create a chat room endpoint.

-

Delete a chat room

-

Endpoint to delete a chat room.

-
-

DELETE /chatrooms/{roomName}

-
-

Payload: none

-

Return value: HTTP status 200 (OK)

-

Possible parameters

- - - - - - - - - - - - - - - - - - - - - - - - -
ParameterParameter TypeDescriptionDefault value
roomname@PathExact room name
servicename@QueryParamThe name of the Group Chat Serviceconference

Examples

-
-

Header: Authorization: Basic YWRtaW46MTIzNDU=

-

DELETE http://example.org:9090/plugins/restapi/v1/chatrooms/testroom

-

DELETE http://example.org:9090/plugins/restapi/v1/chatrooms/testroom?servicename=privateconf

-
-

Update a chat room

-

Endpoint to update a chat room.

-
-

PUT /chatrooms/{roomName}

-
-

Payload: Chatroom

-

Return value: HTTP status 200 (OK)

-

Possible parameters

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
ParameterParameter TypeDescriptionDefault value
roomname@PathExact room name
servicename@QueryParamThe name of the Group Chat Serviceconference
sendInvitations@QueryParamWhether to send invitations to newly affiliated usersfalse

Examples

-
-

Header: Authorization: Basic YWRtaW46MTIzNDU=

-

Header: Content-Type application/xml

-

PUT http://example.org:9090/plugins/restapi/v1/chatrooms/global

-
-

Payload:

-
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
-<chatRoom>  
- <roomName>global</roomName> <naturalName>global-2</naturalName> <description>Global Chat Room edit</description> <subject>New subject</subject> <password>test</password> <creationDate>2014-02-12T15:52:37.592+01:00</creationDate> <modificationDate>2014-09-12T14:20:56.286+02:00</modificationDate> <maxUsers>0</maxUsers> <persistent>true</persistent> <publicRoom>true</publicRoom> <registrationEnabled>false</registrationEnabled> <canAnyoneDiscoverJID>false</canAnyoneDiscoverJID> <canOccupantsChangeSubject>false</canOccupantsChangeSubject> <canOccupantsInvite>false</canOccupantsInvite> <canChangeNickname>false</canChangeNickname> <logEnabled>true</logEnabled> <loginRestrictedToNickname>false</loginRestrictedToNickname> <membersOnly>false</membersOnly> <moderated>false</moderated> <allowPM>anyone</allowPM> <broadcastPresenceRoles/> <owners> <owner>owner@localhost</owner> </owners> <admins> <admin>admin@localhost</admin> </admins> <members> <member>member2@localhost</member> <member>member1@localhost</member> </members> <outcasts> <outcast>outcast1@localhost</outcast> </outcasts></chatRoom>  
-
-

Invite user or user group to a chat Room

-

Endpoint to invite a user or a user group to a room.

-
-

Header: Authorization: Basic YWRtaW46MTIzNDU=

-

Header: Content-Type: application/xml

-

POST http://localhost:9090/plugins/restapi/v1/chatrooms/{roomName}/invite/{name}

-
-

Payload Example:

-
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
-<mucInvitation>  
- <reason>Hello, come to this room, it is nice</reason></mucInvitation>  
-
-

Return value: HTTP status 200 (OK)

-

Possible parameters

- - - - - - - - - - - - - - - - - - - - - - - - -
ParameterParameter TypeDescriptionDefault value
roomname@PathExact room name
name@PathThe local username or group name or the user JID or group JID

Invite multiple users and/or user groups to a chat Room

-

Endpoint to invite multiple users and/or user groups to a room. Works both with JIDs and (user/group) names.

-
-

Header: Authorization: Basic YWRtaW46MTIzNDU=

-

Header: Content-Type: application/xml

-

POST http://localhost:9090/plugins/restapi/v1/chatrooms/{roomName}/invite

-
-

Payload Example:

-
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
-<mucInvitation>  
- <reason>Hello, come to this room, it is nice</reason> <jidsToInvite> <jid>jane@example.org</jid> <jid>ADNMQP8=@example.org/695c6ae413c00446733d926ccadefd8b</jid> <jid>john</jid> <jid>SomeGroupName</jid> </jidsToInvite></mucInvitation>  
-
-

Return value: HTTP status 200 (OK)

-

Possible parameters

- - - - - - - - - - - - - - - - - - -
ParameterParameter TypeDescriptionDefault value
roomname@PathExact room name

Get all users with a particular affiliation in a chat room

-

Retrieves a list of JIDs for all users with the specified affiliation in a multi-user chat room.

-
-

GET /chatrooms/{roomName}/{affiliation}

-
-

Payload: none

-

Return value: HTTP status 200 (OK)

-

Possible parameters

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
ParameterParameter TypeDescriptionDefault value
roomname@PathExact room name
affiliation@PathAvailable affiliations:
owners
admins
members
outcasts
servicename@QueryParamThe name of the Group Chat Serviceconference

Examples

-
-

Header: Authorization: Basic YWRtaW46MTIzNDU=

-

Header: Content-Type application/xml

-

GET http://example.org:9090/plugins/restapi/v1/chatrooms/global/member

-
-

Return payload:

-
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
-<members>  
- <member>member2@localhost</member> <member>member1@localhost</member></members>  
-
-

Add user with affiliation to chat room

-

Endpoint to add a new user with affiliation to a room.

-
-

POST /chatrooms/{roomName}/{affiliation}/{name}

-
-

Payload: none

-

Return value: HTTP status 201 (Created)

-

Possible parameters

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
ParameterParameter TypeDescriptionDefault value
roomname@PathExact room name
name@PathThe local username or the user JID
affiliation@PathAvailable affiliations:
owners
admins
members
outcasts
servicename@QueryParamThe name of the Group Chat Serviceconference
sendInvitations@QueryParamWhether to send invitation to the newly affiliated userfalse

Examples

-
-

Header: Authorization: Basic YWRtaW46MTIzNDU=

-

Header: Content-Type application/xml

-

POST http://example.org:9090/plugins/restapi/v1/chatrooms/global/owners/testUser

-

POST http://example.org:9090/plugins/restapi/v1/chatrooms/global/owners/testUser@openfire.com

-

POST http://example.org:9090/plugins/restapi/v1/chatrooms/global/admins/testUser

-

POST http://example.org:9090/plugins/restapi/v1/chatrooms/global/members/testUser?sendInvitations=true

-

POST http://example.org:9090/plugins/restapi/v1/chatrooms/global/outcasts/testUser

-

POST http://example.org:9090/plugins/restapi/v1/chatrooms/global/owners/testUser?servicename=privateconf

-
-

Replace all users with a affiliation in a chat room

-

Endpoint to replace all users with a particular affiliation in a multi-user chat room. Note that a user can only have one type of affiliation with a room. By adding a user using a particular affiliation, any other pre-existing affiliation is removed.

-
-

PUT /chatrooms/{roomName}/{affiliation}

-
-

Payload: list of affiliations

-

Return value: HTTP status 201 (Created)

-

Possible parameters

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
ParameterParameter TypeDescriptionDefault value
roomname@PathExact room name
affiliation@PathAvailable affiliations:
owners
admins
members
outcasts
servicename@QueryParamThe name of the Group Chat Serviceconference
sendInvitations@QueryParamWhether to send invitation to newly affiliated usersfalse

Examples

-
-

Header: Authorization: Basic YWRtaW46MTIzNDU=

-

Header: Content-Type application/xml

-

PUT http://example.org:9090/plugins/restapi/v1/chatrooms/global/members

-
-

Request Payload:

-
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
-<members>  
- <member>member2@localhost</member> <member>member1@localhost</member></members>  
-
-

Add multiple users with a affiliation to a chat room

-

Endpoint to add multiple users with an affiliation to a multi-user chat room. Note that a user can only have one type of affiliation with a room. By adding a user using a particular affiliation, any other pre-existing affiliation is removed.

-
-

PUT /chatrooms/{roomName}/{affiliation}

-
-

Payload: list of affiliations

-

Return value: HTTP status 201 (Created)

-

Possible parameters

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
ParameterParameter TypeDescriptionDefault value
roomname@PathExact room name
affiliation@PathAvailable affiliation:
owners
admins
members
outcasts
servicename@QueryParamThe name of the Group Chat Serviceconference
sendInvitations@QueryParamWhether to send invitations to newly affiliated usersfalse

Examples

-
-

Header: Authorization: Basic YWRtaW46MTIzNDU=

-

Header: Content-Type application/xml

-

POST http://example.org:9090/plugins/restapi/v1/chatrooms/global/members

-
-

Request Payload:

-
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
-<members>  
- <member>member2@localhost</member> <member>member1@localhost</member></members>  
-
-

Add group with affiliation to chat room

-

Endpoint to add a new group with affiliation to a room.

-
-

POST /chatrooms/{roomName}/{affiliation}/group/{name}

-
-

Payload: none

-

Return value: HTTP status 201 (Created)

-

Possible parameters

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
ParameterParameter TypeDescriptionDefault value
roomname@PathExact room name
name@PathThe group name
affiliation@PathAvailable affiliations:
owners
admins
members
outcasts
servicename@QueryParamThe name of the Group Chat Serviceconference
sendInvitations@QueryParamWhether to send invitations to the users in the newly affiliated groupsfalse

Examples

-
-

Header: Authorization: Basic YWRtaW46MTIzNDU=

-

Header: Content-Type application/xml

-

POST http://example.org:9090/plugins/restapi/v1/chatrooms/global/owners/group/testGroup

-

POST http://example.org:9090/plugins/restapi/v1/chatrooms/global/admins/group/testGroup

-

POST http://example.org:9090/plugins/restapi/v1/chatrooms/global/members/group/testGroup

-

POST http://example.org:9090/plugins/restapi/v1/chatrooms/global/outcasts/group/testGroup

-

POST http://example.org:9090/plugins/restapi/v1/chatrooms/global/owners/group/testUser?servicename=privateconf

-
-

Delete a user from a chat room

-

Endpoint to remove a room user affiliation.

-
-

DELETE /chatrooms/{roomName}/{affiliations}/{name}

-
-

Payload: none

-

Return value: HTTP status 200 (OK)

-

Possible parameters

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
ParameterParameter TypeDescriptionDefault value
roomname@PathExact room name
name@PathThe local username or the user JID
affiliations@PathAvailable affiliations:
owners
admins
members
outcasts
servicename@QueryParamThe name of the Group Chat Serviceconference

Examples

-
-

Header: Authorization: Basic YWRtaW46MTIzNDU=

-

Header: Content-Type application/xml

-

DELETE http://example.org:9090/plugins/restapi/v1/chatrooms/global/owners/testUser

-

DELETE http://example.org:9090/plugins/restapi/v1/chatrooms/global/owners/testUser@openfire.com

-

DELETE http://example.org:9090/plugins/restapi/v1/chatrooms/global/admins/testUser

-

DELETE http://example.org:9090/plugins/restapi/v1/chatrooms/global/members/testUser

-

DELETE http://example.org:9090/plugins/restapi/v1/chatrooms/global/outcasts/testUser

-

DELETE http://example.org:9090/plugins/restapi/v1/chatrooms/global/owners/testUser?servicename=privateconf

-
-

System related REST Endpoints

-

Retrieve all system properties

-

Endpoint to get all system properties

-
-

GET /system/properties

-
-

Payload: none

-

Return value: System properties

-

Examples

-
-

Header: Authorization: Basic YWRtaW46MTIzNDU=

-

GET http://example.org:9090/plugins/restapi/v1/system/properties

-
-

Retrieve system property

-

Endpoint to get information over specific system property

-
-

GET /system/properties/{propertyName}

-
-

Payload: none

-

Return value: System property

-

Possible parameters

- - - - - - - - - - - - - - - - - - -
ParameterParameter TypeDescriptionDefault value
propertyName@PathThe name of system property

Examples

-
-

Header: Authorization: Basic YWRtaW46MTIzNDU=

-

GET http://example.org:9090/plugins/restapi/v1/system/properties/xmpp.domain

-
-

Create a system property

-

Endpoint to create a system property. Note that the name of the property must consist of one or more dot-separated parts, each consisting of ASCII letters, digits, underscores, apostrophes and hyphens.

-
-

POST system/properties

-
-

Payload: System Property

-

Return value: HTTP status 201 (Created)

-

Examples

-
-

Header: Authorization: Basic YWRtaW46MTIzNDU=

-

Header: Content-Type: application/xml

-

POST http://example.org:9090/plugins/restapi/v1/system/properties

-
-

Payload Example:

-
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
-<property key="propertyName" value="propertyValue"/>  
-
-

Delete a system property

-

Endpoint to delete a system property, together with all of its child properties (properties of which the name starts with the name of this property, followed by a dot). Note that the name of the property must consist of one or more dot-separated parts, each consisting of ASCII letters, digits, underscores, apostrophes and hyphens. A deletion that could also delete other properties (for example, because the name contains an underscore, which can match any character) is rejected.

-
-

DELETE /system/properties/{propertyName}

-
-

Payload: none

-

Return value: HTTP status 200 (OK)

-

Possible parameters

- - - - - - - - - - - - - - - - - - -
ParameterParameter TypeDescriptionDefault value
propertyName@PathThe name of system property

Examples

-
-

Header: Authorization: Basic YWRtaW46MTIzNDU=

-

DELETE http://example.org:9090/plugins/restapi/v1/system/properties/propertyName

-
-

Update a system property

-

Endpoint to update / overwrite a system property

-
-

PUT /system/properties/{propertyName}

-
-

Payload: System property

-

Return value: HTTP status 200 (OK)

-

Possible parameters

- - - - - - - - - - - - - - - - - - -
ParameterParameter TypeDescriptionDefault value
propertyName@PathThe name of system property

Examples

-
-

Header: Authorization: Basic YWRtaW46MTIzNDU=

-

Header: Content-Type application/xml

-

PUT http://example.org:9090/plugins/restapi/v1/system/properties/propertyName

-
-

Payload:

-
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
-<property key="propertyName" value="anotherValue"/>  
-
-

Retrieve concurrent sessions

-

Endpoint to get count of concurrent sessions

-
-

GET /system/statistics/sessions

-
-

Payload: none

-

Return value: Sessions count

-

Examples

-
-

Header: Authorization: Basic YWRtaW46MTIzNDU=

-

GET http://example.org:9090/plugins/restapi/v1/system/statistics/sessions

-
-

Check the ‘liveness’ state (using all checks)

-

Detects if Openfire has reached a state that it cannot recover from, except for with a restart, based on every liveness check that it has implemented.

-
-

GET /system/liveness

-
-

Payload: none

-

Return value: HTTP status 200 (OK). Any HTTP status outside the range 200-399 indicates failure.

-

Perform ‘deadlock’ liveness check

-

Detects if Openfire has reached a state that it cannot recover from because of a deadlock.

-
-

GET /system/liveness/deadlock

-
-

Payload: none

-

Return value: HTTP status 200 (OK). Any HTTP status outside the range 200-399 indicates failure.

-

Perform ‘properties’ liveness check

-

Detects if Openfire has reached a state that it cannot recover from because a system property change requires a restart to take effect.

-
-

GET /system/liveness/properties

-
-

Payload: none

-

Return value: HTTP status 200 (OK). Any HTTP status outside the range 200-399 indicates failure.

-

Check the ‘readiness’ state (using all checks)

-

Detects if Openfire is in a state where it is ready to process traffic, based on every readiness check that it has implemented.

-
-

GET /system/readiness

-
-

Payload: none

-

Return value: HTTP status 200 (OK). Any HTTP status outside the range 200-399 indicates failure.

-

Perform ‘server’ readiness check

-

Detects if Openfire’s core service has been started.

-
-

GET /system/readiness/server

-
-

Payload: none

-

Return value: HTTP status 200 (OK). Any HTTP status outside the range 200-399 indicates failure.

-

Perform ‘cluster’ readiness check

-

Detects if the cluster functionality has finished starting (or is disabled).

-
-

GET /system/readiness/cluster

-
-

Payload: none

-

Return value: HTTP status 200 (OK). Any HTTP status outside the range 200-399 indicates failure.

-

Perform ‘plugins’ readiness check

-

Detects if Openfire has finished starting its plugins.

-
-

GET /system/readiness/plugins

-
-

Payload: none

-

Return value: HTTP status 200 (OK). Any HTTP status outside the range 200-399 indicates failure.

-

Perform ‘connections’ readiness check

-

Detects if Openfire is ready to accept connection requests.

-
-

GET /system/readiness/connections

-
-

Payload: none

-

Return value: HTTP status 200 (OK). Any HTTP status outside the range 200-399 indicates failure.

-

Possible parameters

- - - - - - - - - - - - - - - - - - - - - - - - -
ParameterParameter TypeDescriptionDefault value
connectionType@PathOptional. Use to limit the check to one particular connection type. One of: SOCKET_S2S, SOCKET_C2S, BOSH_C2S, WEBADMIN, COMPONENT, CONNECTION_MANAGER
encypted@PathCheck the encrypted (true) or unencrypted (false) variant of the connection type. Only used in combination with ‘connectionType’, as without it, all types and both encrypted and unencrypted are checked.

Group related REST Endpoints

-

Retrieve all groups

-

Endpoint to get all groups

-
-

GET /groups

-
-

Payload: none

-

Return value: Groups

-

Examples

-
-

Header: Authorization: Basic YWRtaW46MTIzNDU=

-

GET http://example.org:9090/plugins/restapi/v1/groups

-
-

Retrieve a group

-

Endpoint to get information over specific group

-
-

GET /groups/{groupName}

-
-

Payload: none

-

Return value: Group

-

Possible parameters

- - - - - - - - - - - - - - - - - - -
ParameterParameter TypeDescriptionDefault value
groupName@PathThe name of the group

Examples

-
-

Header: Authorization: Basic YWRtaW46MTIzNDU=

-

GET http://example.org:9090/plugins/restapi/v1/groups/moderators

-
-

Create a group

-

Endpoint to create a new group

-
-

POST /groups

-
-

Payload: Group

-

Return value: HTTP status 201 (Created)

-

Examples

-
-

Header: Authorization: Basic YWRtaW46MTIzNDU=

-

Header: Content-Type: application/xml

-

POST http://example.org:9090/plugins/restapi/v1/groups

-
-

Payload Example:

-
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
-<group>  
- <name>GroupName</name> <description>Some description</description> <isshared>false</isshared></group>  
-
-

Delete a group

-

Endpoint to delete a group

-
-

DELETE /groups/{groupName}

-
-

Payload: none

-

Return value: HTTP status 200 (OK)

-

Possible parameters

- - - - - - - - - - - - - - - - - - -
ParameterParameter TypeDescriptionDefault value
groupName@PathThe name of the group

Examples

-
-

Header: Authorization: Basic YWRtaW46MTIzNDU=

-

DELETE http://example.org:9090/plugins/restapi/v1/groups/groupToDelete

-
-

Update a group

-

Endpoint to update / overwrite a group

-
-

PUT /groups/{groupName}

-
-

Payload: Group

-

Return value: HTTP status 200 (OK)

-

Possible parameters

- - - - - - - - - - - - - - - - - - -
ParameterParameter TypeDescriptionDefault value
groupName@PathThe name of the group

Examples

-
-

Header: Authorization: Basic YWRtaW46MTIzNDU=

-

Header: Content-Type application/xml

-

PUT http://example.org:9090/plugins/restapi/v1/groups/groupNameToUpdate

-
-

Payload:

-
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
-<group>  
- <name>groupNameToUpdate</name> <description>New description</description> <isshared>false</isshared></group>  
-
-

Session related REST Endpoints

-

Retrieve all user session

-

Endpoint to get all user sessions

-
-

GET /sessions

-
-

Payload: none

-

Return value: Sessions

-

Examples

-
-

Header: Authorization: Basic YWRtaW46MTIzNDU=

-

GET http://example.org:9090/plugins/restapi/v1/sessions

-
-

Retrieve the user sessions

-

Endpoint to get sessions from a user

-
-

GET /sessions/{username}

-
-

Payload: none

-

Return value: Sessions

-

Possible parameters

- - - - - - - - - - - - - - - - - - -
ParameterParameter TypeDescriptionDefault value
username@PathThe username of the user

Examples

-
-

Header: Authorization: Basic YWRtaW46MTIzNDU=

-

GET http://example.org:9090/plugins/restapi/v1/sessions/testuser

-
-

Close all user sessions

-

Endpoint to close/kick sessions from a user

-
-

DELETE /sessions/{username}

-
-

Payload: none

-

Return value: HTTP status 200 (OK)

-

Possible parameters

- - - - - - - - - - - - - - - - - - -
ParameterParameter TypeDescriptionDefault value
username@PathThe username of the user

Examples

-
-

Header: Authorization: Basic YWRtaW46MTIzNDU=

-

DELETE http://example.org:9090/plugins/restapi/v1/sessions/testuser

-
-

Message related REST Endpoints

-

Send a broadcast message

-

Endpoint to send a broadcast/server message to all online users

-
-

POST /messages/users

-
-

Payload: Message

-

Return value: HTTP status 201 (Created)

-

Examples

-
-

Header: Authorization: Basic YWRtaW46MTIzNDU=

-

POST http://example.org:9090/plugins/restapi/v1/messages/users

-
-

Payload:

-
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
-<message>  
- <body>Your message</body></message>  
-
-

Security Audit related REST Endpoints

-

Retrieve the Security audit logs

-

Endpoint to get security audit logs

-
-

GET /logs/security

-
-

Payload: none

-

Return value: Security Audit Logs

-

Possible parameters

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
ParameterParameter TypeDescriptionDefault value
username@QueryParamUsername of user to look up
startTime@QueryParamOldest timestamp of range of logs to retrieve
endTime@QueryParamMost recent timestamp of range of logs to retrieve0 (until now)
offset@QueryParamNumber of logs to skip
limit@QueryParamNumber of logs to retrieve

Examples

-
-

Header: Authorization: Basic YWRtaW46MTIzNDU=

-

GET http://example.org:9090/plugins/restapi/v1/logs/security

-
-

Clustering related REST Endpoints

-

Retrieve information for all cluster nodes.

-

Endpoint to get information for all nodes in the cluster. Note that this endpoint can only return data for remote nodes
- when the instance of Openfire that processes this query has successfully joined the cluster.

-
-

GET http://example.org:9090/plugins/restapi/v1/clustering/nodes

-
-

Payload: none

-

Return value: ClusterNodes

-

Examples

-
-

Header: Authorization: Basic YWRtaW46MTIzNDU=

-

GET http://example.org:9090/plugins/restapi/v1/clustering/nodes

-
-

Retrieve information for a specific cluster node.

-

Endpoint to get information for a specific cluster node. Note that this endpoint can only return data for remote nodes
- when the instance of Openfire that processes this query has successfully joined the cluster.

-
-

GET http://example.org:9090/plugins/restapi/v1/clustering/nodes/{nodeId}

-
-

Payload: none

-

Return value: ClusterNode

-

Possible parameters

- - - - - - - - - - - - - - - - - - -
ParameterParameter TypeDescriptionDefault value
nodeId@PathExact NodeID

Examples

-
-

Header: Authorization: Basic YWRtaW46MTIzNDU=

-

GET http://example.org:9090/plugins/restapi/v1/clustering/nodes/52a89928-66f7-45fd-9bb8-096de07400ac

-
-

Retrieve the Clustering status

-

Endpoint to get description of clustering status

-
-

GET /clustering/status

-
-

Payload: none

-

Return value: String describing the clustering status of this Openfire instance

-

Examples

-
-

Header: Authorization: Basic YWRtaW46MTIzNDU=

-

GET http://example.org:9090/plugins/restapi/v1/clustering/status

-
-

Possible Responses

-
    -
  • SENIOR AND ONLY MEMBER
  • -
  • Senior member
  • -
  • Junior member
  • -
  • Starting up
  • -
  • Disabled
  • -
-

Data format

-

Openfire REST API provides XML and JSON as data format. The default data format is XML.
- To get a JSON result, please add “Accept: application/json” to the request header.
- If you want to create a resource with JSON data format, please add “Content-Type: application/json”.

-

Data types

-

ClusterNode

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
ParameterOptionalDescription
hostNameNoThe hostname and IP address of the server on which this cluster node is running.
nodeIDNoA unique identifier of this cluster node.
joinedTimeNoTimestamp when the node joined the cluster.
seniorMemberNoBoolean value indicating if the node is currently the senior member of the cluster.

User

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
ParameterOptionalDescription
usernameNoThe username of the user
nameYesThe name of the user
emailYesThe email of the user
passwordNoThe password of the user
propertiesYesList of properties. Property is a key / value object. The key must to be per user unique

RosterItem

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
ParameterOptionalDescription
jidNoThe JID of the roster item
nicknameYesThe nickname for the user when used in this roster
subscriptionTypeYesThe subscription type
Possible numeric values are: -1 (remove), 0 (none), 1 (to), 2 (from), 3 (both)
groupsNoA list of groups to organize roster entries under (e.g. friends, co-workers, etc.)

Chatroom

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
ParameterOptionalDescription
roomNameNoThe name/id of the room. Can only contains lowercase and alphanumeric characters.
naturalNameNoAlso the name of the room, but can contains non alphanumeric characters. It’s mainly used for users while discovering rooms hosted by the Multi-User Chat service.
descriptionNoDescription text of the room.
subjectYesSubject of the room.
passwordYesThe password that the user must provide to enter the room
creationDateYesThe date when the room was created. Will be automatically set by creation. Example: 2014-07-10T09:49:12.411+02:00
modificationDateYesThe last date when the room’s configuration was modified. If the room’s configuration was never modified then the initial value will be the same as the creation date. Will be automatically set by update. Example: 2014-07-10T09:49:12.411+02:00
maxUsersYesthe maximum number of occupants that can be simultaneously in the room. 0 means unlimited number of occupants.
persistentYesCan be “true” or “false”. Persistent rooms are saved to the database to make their configurations persistent together with the affiliation of the users. Otherwise the room will be destroyed if the last occupant leave the room.
publicRoomYesCan be “true” or “false”. True if the room is searchable and visible through service discovery.
registrationEnabledYesCan be “true” or “false”. True if users are allowed to register with the room. By default, room registration is enabled.
canAnyoneDiscoverJIDYesCan be “true” or “false”. True if every presence packet will include the JID of every occupant.
canOccupantsChangeSubjectYesCan be “true” or “false”. True if participants are allowed to change the room’s subject.
canOccupantsInviteYesCan be “true” or “false”. True if occupants can invite other users to the room. If the room does not require an invitation to enter (i.e. is not members-only) then any occupant can send invitations. On the other hand, if the room is members-only and occupants cannot send invitation then only the room owners and admins are allowed to send invitations.
canChangeNicknameYesCan be “true” or “false”. True if room occupants are allowed to change their nicknames in the room. By default, occupants are allowed to change their nicknames.
logEnabledYesCan be “true” or “false”. True if the room’s conversation is being logged. If logging is activated the room conversation will be saved to the database every couple of minutes. The saving frequency is the same for all the rooms and can be configured by changing the property “xmpp.muc.tasks.log.timeout”.
loginRestrictedToNicknameYesCan be “true” or “false”. True if registered users can only join the room using their registered nickname. By default, registered users can join the room using any nickname.
membersOnlyYesCan be “true” or “false”. True if the room requires an invitation to enter. That is if the room is members-only.
moderatedYesCan be “true” or “false”. True if the room in which only those with “voice” may send messages to all occupants.
allowPMYesOne of “anyone”, “participants”, “moderators” or “none”. Controls who is allowed to send private messages to other occupants in the room.
broadcastPresenceRolesYesThe list of roles of which presence will be broadcasted to the rest of the occupants.
ownersYesA collection with the current list of owners. The collection contains the bareJID of the users with owner affiliation.
adminsYesA collection with the current list of admins. The collection contains the bareJID of the users with admin affiliation.
membersYesA collection with the current list of room members. The collection contains the bareJID of the users with member affiliation. If the room is not members-only then the list will contain the users that registered with the room and therefore they may have reserved a nickname.
outcastsYesA collection with the current list of outcast users. An outcast user is not allowed to join the room again. The collection contains the bareJID of the users with outcast affiliation.
ownerGroupsYesA collection with the current list of groups with owner affiliation. The collection contains the name only.
adminGroupsYesA collection with the current list of groups with admin affiliation. The collection contains the name only.
memberGroupsYesA collection with the current list of groups with member affiliation. The collection contains the name only.
outcastGroupsYesA collection with the current list of groups with outcast affiliation. The collection contains the name only.

Group

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
ParameterOptionalDescription
nameNoThe name of the group
descriptionNoThe description of the group
adminsYesA collection with current admins of the group
membersYesA collection with current members of the group

System Property

- - - - - - - - - - - - - - - - - - - - - -
ParameterOptionalDescription
keyNoThe name of the system property
valueNoThe value of the system property

Session

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
ParameterOptionalDescription
sessionIdNoFull JID of a user e.g. (testUser@testserver.de/SomeRessource)
usernameNoThe username associated with this session. Can be also “Anonymous”.
resourceYesResource name
nodeNoCan be “Local” or “Remote”
sessionStatusNoThe current status of this session. Can be “Closed”, “Connected”, “Authenticated” or “Unknown”.
presenceStatusNoThe status of this presence packet, a natural-language description of availability status.
priorityNoThe priority of the session. The valid priority range is -128 through 128.
hostAddressNoThe IP address string in textual presentation.
hostNameNoThe host name for this IP address.
creationDateNoThe date the session was created.
lastActionDateNoThe time the session last had activity.
secureNoIs “true” if this connection is secure.

Sessions count

- - - - - - - - - - - - - - - - - - - - - -
ParameterOptionalDescription
clusterSessionsNoNumber of client sessions that are authenticated with the server. This includes anonymous and non-anoymous users from the whole cluster.
localSessionsNoNumber of client sessions that are authenticated with the server. This includes anonymous and non-anoymous users.

Security Audit Logs

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
ParameterOptionalDescription
logIdNoUnique ID of this log
usernameNoThe username of the user who performed this event
timestampNoThe time stamp of when this event occurred
summaryNoThe summary, or short description of what transpired in the event
nodeNoThe node that triggered the event, usually a hostname or IP address
detailsNoDetailed information about what occurred in the event

Occupants

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
ParameterOptionalDescription
jidNoThe JID of the MUC room
userAddressNoThe JID of the user
roleNoRole of the user
affiliationNoAffiliation of the user
-
-
- - - diff --git a/src/readme/footer.html b/src/readme/footer.html new file mode 100644 index 000000000..f7ae9d4eb --- /dev/null +++ b/src/readme/footer.html @@ -0,0 +1,70 @@ + + + + diff --git a/src/readme/header.html b/src/readme/header.html new file mode 100644 index 000000000..a6a6713b2 --- /dev/null +++ b/src/readme/header.html @@ -0,0 +1,95 @@ + + + + + + + titleToken + + + + + +
From dcb529e13479e647ce79986de9575a0971d006fa Mon Sep 17 00:00:00 2001 From: Guus der Kinderen Date: Fri, 25 Sep 2026 20:40:51 +0200 Subject: [PATCH 2/4] Generate the endpoint documentation in the readme from the OpenAPI annotations The endpoint documentation in readme.md was maintained by hand, and was often out of date with the implementation. The build now generates an OpenAPI specification from the annotations in the source code (in the process-classes phase), and uses that to replace the endpoint documentation in readme.md (between the 'GENERATED ENDPOINTS' markers). A CI job fails when the committed readme.md does not match the generated documentation. Details that were documented only in the readme (clustering status values, and the ability to use names instead of JIDs to identify users and groups for invitations and affiliations) have been moved into the annotations. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/build.yml | 23 + pom.xml | 52 + readme.md | 2655 +++++++---------- src/build/ReadmeEndpointsGenerator.java | 277 ++ .../plugin/rest/entity/ClusteringEntity.java | 3 + .../rest/service/ClusteringService.java | 2 +- .../service/MUCRoomAffiliationsService.java | 4 +- .../plugin/rest/service/MUCRoomService.java | 4 +- 8 files changed, 1369 insertions(+), 1651 deletions(-) create mode 100644 src/build/ReadmeEndpointsGenerator.java diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 3605b4346..4580fd45c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -8,6 +8,29 @@ jobs: secrets: IGNITE_REALTIME_MAVEN_USERNAME: ${{ secrets.IGNITE_REALTIME_MAVEN_USERNAME }} IGNITE_REALTIME_MAVEN_PASSWORD: ${{ secrets.IGNITE_REALTIME_MAVEN_PASSWORD }} + readme-up-to-date: + name: Check that the readme documents the current endpoints + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v7 + + - name: Set up Java + uses: actions/setup-java@v4 + with: + java-version: 17 + distribution: temurin + cache: maven + + - name: Regenerate the endpoint documentation + run: mvn -B -DskipTests process-classes + + - name: Fail when the readme was not up to date + run: | + if ! git diff --exit-code readme.md; then + echo "::error file=readme.md::The endpoint documentation in readme.md does not match the OpenAPI annotations in the source code. Run 'mvn process-classes' (or any later build phase, like 'mvn package') and commit the updated readme.md." + exit 1 + fi hurl-e2e-test: name: Hurl E2E Tests runs-on: ubuntu-latest diff --git a/pom.xml b/pom.xml index e562316f1..6643089b4 100644 --- a/pom.xml +++ b/pom.xml @@ -47,6 +47,58 @@ 3.6.0 + + + + io.swagger.core.v3 + swagger-maven-plugin + ${swagger.version} + + + generate-openapi-spec + process-classes + + resolve + + + + + ${project.build.directory}/openapi + openapi + JSON + true + + org.jivesoftware.openfire.plugin.rest.service + + + + + + org.codehaus.mojo + exec-maven-plugin + 3.6.4 + + + generate-readme-endpoints + process-classes + + exec + + + ${java.home}/bin/java + + -Dslf4j.internal.verbosity=ERROR + -classpath + + ${project.basedir}/src/build/ReadmeEndpointsGenerator.java + ${project.build.directory}/openapi/openapi.json + ${project.basedir}/readme.md + + + + + + diff --git a/readme.md b/readme.md index bacf218d7..80492053a 100644 --- a/readme.md +++ b/readme.md @@ -132,1980 +132,1343 @@ Be aware of the following: client's address. - Changes to the `adminConsole.forwarded.*` properties take effect only after the admin console has been restarted. -# User related REST Endpoints + -## Retrieve users -Endpoint to get all or filtered users -> **GET** /users +# REST Endpoints -**Payload:** none +The paths of all endpoints below are relative to the root of the Openfire admin console, for example `http://example.org:9090`. -**Return value:** Users +In addition to the responses that are documented for each endpoint, every endpoint can respond with: -### Possible parameters +- `401`: Web service authentication failed. +- `500`: Unexpected, generic error condition. -| Parameter | Parameter Type | Description | Default value | -|---------------|----------------|--------------------------------------------------------------------------------------------------------------|---------------| -| search | @QueryParam | Search/Filter by username.
This act like the wildcard search %String% | | -| propertyKey | @QueryParam | Filter by user propertyKey. | | -| propertyValue | @QueryParam | Filter by user propertyKey and propertyValue.
**Note:** It can only be used within propertyKey parameter | | +Interactive documentation of these endpoints is available in the Openfire admin console, via the link on the REST API settings page (Server > Server Settings > REST API). -### Examples +# Users ->**Header**: Authorization: Basic YWRtaW46MTIzNDU= +Managing Openfire users. ->**GET** http://example.org:9090/plugins/restapi/v1/users +## Get users ->**GET** http://example.org:9090/plugins/restapi/v1/users?search=testuser +> **GET** /plugins/restapi/v1/users ->**GET** http://example.org:9090/plugins/restapi/v1/users?propertyKey=keyname +Retrieve all users defined in Openfire (with optional filtering). ->**GET** http://example.org:9090/plugins/restapi/v1/users?propertyKey=keyname&propertyValue=keyvalue +**Parameters** -If you want to get a JSON format result, please add "**Accept: application/json**" to the **Header**. +| Name | Located in | Required | Description | Default value | +|------|------------|----------|-------------|---------------| +| search | query | no | Search/Filter by username. This acts like the wildcard search %String%. | | +| propertyKey | query | no | Filter by a user property name. | | +| propertyValue | query | no | Filter by user property value. Note: This can only be used in combination with a property name parameter. | | -## Retrieve a user -Endpoint to get information over a specific user -> **GET** /users/{username} +**Responses** -**Payload:** none +| Status | Description | Response body | +|--------|-------------|---------------| +| 200 | A list of Openfire users. | `UserEntities` (XML or JSON) | -**Return value:** User +## Create user -### Possible parameters +> **POST** /plugins/restapi/v1/users -| Parameter | Parameter Type | Description | Default value | -|-----------|----------------|------------------|---------------| -| username | @Path | Exact username | | +Add a new user to Openfire. -### Examples +**Request body** (required): `UserEntity` (XML or JSON) - The definition of the user to create. ->**Header:** Authorization: Basic YWRtaW46MTIzNDU= -> ->**GET** http://example.org:9090/plugins/restapi/v1/users/testuser +**Responses** -## Create a user -Endpoint to create a new user -> **POST** /users +| Status | Description | Response body | +|--------|-------------|---------------| +| 201 | The user was created. | | +| 400 | No user definition, username or password was provided. | `ErrorResponse` | +| 409 | A user with this username already exists. | `ErrorResponse` | -**Payload:** User -**Return value:** HTTP status 201 (Created) +## Get user -### Examples -#### XML Examples +> **GET** /plugins/restapi/v1/users/{username} +Retrieve a user that is defined in Openfire. ->**Header:** Authorization: Basic YWRtaW46MTIzNDU= -> ->**Header:** Content-Type: application/**xml** -> ->**POST** http://example.org:9090/plugins/restapi/v1/users - -**Payload Example 1 (required parameters):** - -```xml - - - test3 - p4ssword - -``` - -**Payload Example 2 (available parameters):** -```xml - - - testuser - p4ssword - Test User - test@localhost.de - - - - - -``` -#### JSON Examples ->**Header:** Authorization: Basic YWRtaW46MTIzNDU= -> ->**Header:** Content-Type: application/**json** -> ->**POST** http://example.org:9090/plugins/restapi/v1/users - -**Payload Example 1 (required parameters):** -```json -{ - "username": "admin", - "password": "p4ssword" -} -``` - -**Payload Example 2 (available parameters):** -```json -{ - "username": "admin", - "password": "p4ssword", - "name": "Administrator", - "email": "admin@example.com", - "properties": { - "property": [ - { - "@key": "console.rows_per_page", - "@value": "user-summary=8" - }, - { - "@key": "console.order", - "@value": "session-summary=1" - } - ] - } -} -``` - -**REST API Version 1.3.0 and later - Payload Example 3 (available parameters):** -```json -{ - "users": [ - { - "username": "admin", - "name": "Administrator", - "email": "admin@example.com", - "password": "p4ssword", - "properties": [ - { - "key": "console.order", - "value": "session-summary=0" - } - ] - }, - { - "username": "test", - "name": "Test", - "password": "p4ssword" - } - ] -} -``` - -## Delete a user -Endpoint to delete a user -> **DELETE** /users/{username} - -**Payload:** none - -**Return value:** HTTP status 200 (OK) - -### Possible parameters - -| Parameter | Parameter Type | Description | Default value | -|-----------|-----------------|----------------|---------------| -| username | @Path | Exact username | | - -### Examples +**Parameters** ->**Header:** Authorization: Basic YWRtaW46MTIzNDU= -> ->**DELETE** http://example.org:9090/plugins/restapi/v1/users/testuser +| Name | Located in | Required | Description | Default value | +|------|------------|----------|-------------|---------------| +| username | path | yes | The username of the user to return. | | -## Update a user -Endpoint to update / rename a user -> **PUT** /users/{username} +**Responses** -**Payload:** User +| Status | Description | Response body | +|--------|-------------|---------------| +| 200 | The Openfire user. | `UserEntity` (XML or JSON) | +| 404 | No user with that username was found. | `ErrorResponse` (XML or JSON) | -**Return value:** HTTP status 200 (OK) +## Update user -### Possible parameters +> **PUT** /plugins/restapi/v1/users/{username} -| Parameter | Parameter Type | Description | Default value | -|-----------|-----------------|----------------|---------------| -| username | @Path | Exact username | | +Update an existing user in Openfire. -### Examples -#### XML Example ->**Header:** Authorization: Basic YWRtaW46MTIzNDU= -> ->**Header:** Content-Type application/xml -> ->**PUT** http://example.org:9090/plugins/restapi/v1/users/testuser - -**Payload:** -```xml - - - testuser - Test User edit - test@edit.de - - - - -``` -#### Rename Example +**Parameters** ->**Header:** Authorization: Basic YWRtaW46MTIzNDU= -> ->**Header:** Content-Type application/xml -> ->**PUT** http://example.org:9090/plugins/restapi/v1/users/oldUsername - -**Payload:** -```xml - - - newUsername - Test User edit - test@edit.de - - - - -``` - -#### JSON Example ->**Header:** Authorization: Basic YWRtaW46MTIzNDU= -> ->**Header:** Content-Type application/json -> ->**PUT** http://example.org:9090/plugins/restapi/v1/users/testuser - -**Payload:** -```json -{ - "username": "testuser", - "name": "Test User edit", - "email": "test@edit.de", - "properties": { - "property": { - "@key": "keyname", - "@value": "value" - } - } -} -``` - -**REST API Version 1.3.0 and later - Payload Example 2 (available parameters):** -```json -{ - "username": "testuser", - "name": "Test User edit", - "email": "test@edit.de", - "properties": [ - { - "key": "keyname", - "value": "value" - } - ] -} -``` - -## Retrieve all user groups -Endpoint to get group names of a specific user -> **GET** /users/{username}/groups - -**Payload:** none - -**Return value:** Groups - -### Possible parameters - -| Parameter | Parameter Type | Description | Default value | -|-----------|-----------------|----------------|---------------| -| username | @Path | Exact username | | - -### Examples ->**Header:** Authorization: Basic YWRtaW46MTIzNDU= -> ->**GET** http://example.org:9090/plugins/restapi/v1/users/testuser/groups +| Name | Located in | Required | Description | Default value | +|------|------------|----------|-------------|---------------| +| username | path | yes | The username of the user to update. | | + +**Request body** (required): `UserEntity` (XML or JSON) - The updated definition of the user. + +**Responses** + +| Status | Description | Response body | +|--------|-------------|---------------| +| 200 | The user was updated. | | +| 404 | No user with that username was found. | `ErrorResponse` | +| 409 | The user is to be renamed, but a user with the new username already exists. | `ErrorResponse` | + +## Delete user + +> **DELETE** /plugins/restapi/v1/users/{username} + +Remove an existing user from Openfire. + +**Parameters** + +| Name | Located in | Required | Description | Default value | +|------|------------|----------|-------------|---------------| +| username | path | yes | The username of the user to remove. | | + +**Responses** + +| Status | Description | Response body | +|--------|-------------|---------------| +| 200 | The user was removed. | | +| 404 | No user with that username was found. | `ErrorResponse` | + +## Get user's groups + +> **GET** /plugins/restapi/v1/users/{username}/groups + +Retrieve names of all groups that a particular user is in. + +**Parameters** + +| Name | Located in | Required | Description | Default value | +|------|------------|----------|-------------|---------------| +| username | path | yes | The username of the user for which to return group names. | | + +**Responses** + +| Status | Description | Response body | +|--------|-------------|---------------| +| 200 | The names of the groups that the user is in. | `UserGroupsEntity` (XML or JSON) | +| 404 | No user with that username was found. | `ErrorResponse` (XML or JSON) | ## Add user to groups -Endpoint to add user to a groups -> **POST** /users/{username}/groups -**Payload:** Groups +> **POST** /plugins/restapi/v1/users/{username}/groups -**Return value:** HTTP status 201 (Created) +Add a particular user to a collection of groups. When a group that is provided does not exist, it will be automatically created if possible. -### Possible parameters +**Parameters** +| Name | Located in | Required | Description | Default value | +|------|------------|----------|-------------|---------------| +| username | path | yes | The username of the user that is to be added to groups. | | -| Parameter | Parameter Type | Description | Default value | -|-----------|-----------------|----------------|---------------| -| username | @Path | Exact username | | +**Request body** (required): `UserGroupsEntity` (XML or JSON) - A collection of names for groups that the user is to be added to. -### Examples ->**Header:** Authorization: Basic YWRtaW46MTIzNDU= -> ->**Header:** Content-Type application/xml -> ->**POST** http://example.org:9090/plugins/restapi/v1/users/testuser/groups - -**Payload:** -```xml - - - Admins - Support - -``` +**Responses** + +| Status | Description | Response body | +|--------|-------------|---------------| +| 201 | The user was added to all groups. | | +| 400 | The username cannot be parsed into a JID. | `ErrorResponse` | + +## Delete user from groups + +> **DELETE** /plugins/restapi/v1/users/{username}/groups + +Removes a user from a collection of groups. + +**Parameters** + +| Name | Located in | Required | Description | Default value | +|------|------------|----------|-------------|---------------| +| username | path | yes | The username of the user that is to be removed from groups. | | + +**Request body** (required): `UserGroupsEntity` (XML or JSON) - A collection of names for groups from which the user is to be removed. + +**Responses** + +| Status | Description | Response body | +|--------|-------------|---------------| +| 200 | The user was taken out of the groups. | | +| 404 | One or more groups could not be found. | `ErrorResponse` | ## Add user to group -Endpoint to add user to a group -> **POST** /users/{username}/groups/{groupName} -**Payload:** none +> **POST** /plugins/restapi/v1/users/{username}/groups/{groupName} -**Return value:** HTTP status 201 (Created) +Add a particular user to a particular group. When the group does not exist, it will be automatically created if possible. -### Possible parameters +**Parameters** -| Parameter | Parameter Type | Description | Default value | -|-----------|-----------------|------------------|---------------| -| username | @Path | Exact username | | -| groupName | @Path | Exact group name | | +| Name | Located in | Required | Description | Default value | +|------|------------|----------|-------------|---------------| +| username | path | yes | The username of the user that is to be added to a group. | | +| groupName | path | yes | The name of the group that the user is to be added to. | | -### Examples ->**Header:** Authorization: Basic YWRtaW46MTIzNDU= -> ->**Header:** Content-Type application/xml -> ->**POST** http://example.org:9090/plugins/restapi/v1/users/testuser/groups/testGroup +**Responses** -## Delete a user from a groups -Endpoint to remove a user from a groups ->**DELETE** /users/{username}/groups +| Status | Description | Response body | +|--------|-------------|---------------| +| 201 | The user was added to the group. | | +| 400 | The username cannot be parsed into a JID. | `ErrorResponse` | -**Payload:** Groups +## Delete user from group -**Return value:** HTTP status 200 (OK) +> **DELETE** /plugins/restapi/v1/users/{username}/groups/{groupName} -### Possible parameters +Removes a user from a group. -| Parameter | Parameter Type | Description | Default value | -|-----------|-----------------|----------------|---------------| -| username | @Path | Exact username | | +**Parameters** -### Examples ->**Header:** Authorization: Basic YWRtaW46MTIzNDU= -> ->**Header:** Content-Type application/xml -> ->**DELETE** http://example.org:9090/plugins/restapi/v1/users/testuser/groups +| Name | Located in | Required | Description | Default value | +|------|------------|----------|-------------|---------------| +| username | path | yes | The username of the user that is to be removed from a group. | | +| groupName | path | yes | The name of the group that the user is to be removed from. | | -**Payload:** -```xml - - - Admins - Support - -``` +**Responses** -## Delete a user from a group -Endpoint to remove a user from a group ->**DELETE** /users/{username}/groups/{groupName} +| Status | Description | Response body | +|--------|-------------|---------------| +| 200 | The user was taken out of the group. | | +| 404 | The group could not be found. | `ErrorResponse` | -**Payload:** none +## Retrieve user roster -**Return value:** HTTP status 200 (OK) +> **GET** /plugins/restapi/v1/users/{username}/roster -### Possible parameters +Get a list of all roster entries (buddies / contact list) of a particular user. +**Parameters** -| Parameter | Parameter Type | Description | Default value | -|-----------|-----------------|------------------|---------------| -| username | @Path | Exact username | | -| groupName | @Path | Exact group name | | +| Name | Located in | Required | Description | Default value | +|------|------------|----------|-------------|---------------| +| username | path | yes | The username of the user for which to retrieve the roster entries. | | -### Examples ->**Header:** Authorization: Basic YWRtaW46MTIzNDU= -> ->**Header:** Content-Type application/xml -> ->**DELETE** http://example.org:9090/plugins/restapi/v1/users/testuser/groups/testGroup +**Responses** -## Lockout a user -Endpoint to lockout / ban the user from the chat server. The user will be kicked if the user is online. ->**POST** /lockouts/{username} +| Status | Description | Response body | +|--------|-------------|---------------| +| 200 | All roster entries. | `RosterEntities` (XML or JSON) | +| 404 | No user with this username exists. | `ErrorResponse` (XML or JSON) | -**Payload:** none +## Create roster entry -**Return value:** HTTP status 201 (Created) +> **POST** /plugins/restapi/v1/users/{username}/roster -### Possible parameters +Add a roster entry to the roster (buddies / contact list) of a particular user. -| Parameter | Parameter Type | Description | Default value | -|-----------|-----------------|----------------|---------------| -| username | @Path | Exact username | | +**Parameters** -### Examples ->**Header:** Authorization: Basic YWRtaW46MTIzNDU= -> ->**POST** http://example.org:9090/plugins/restapi/v1/lockouts/testuser +| Name | Located in | Required | Description | Default value | +|------|------------|----------|-------------|---------------| +| username | path | yes | The username of the user for which to add a roster entry. | | -## Unlock a user -Endpoint to unlock / unban the user ->**DELETE** /lockouts/{username} +**Request body** (required): `RosterItemEntity` (XML or JSON) - The definition of the roster entry that is to be added. -**Payload:** none +**Responses** -**Return value:** HTTP status 200 (OK) +| Status | Description | Response body | +|--------|-------------|---------------| +| 201 | The entry was added to the roster. | | +| 400 | A roster entry cannot be added to a 'shared group' (try removing group names from the roster entry and try again). | `ErrorResponse` | +| 404 | No user with this username exists. | `ErrorResponse` | +| 409 | A roster entry already exists for the provided contact JID. | `ErrorResponse` | -### Possible parameters +## Update roster entry -| Parameter | Parameter Type | Description | Default value | -|-----------|-----------------|----------------|---------------| -| username | @Path | Exact username | | +> **PUT** /plugins/restapi/v1/users/{username}/roster/{rosterJid} -### Examples ->**Header:** Authorization: Basic YWRtaW46MTIzNDU= -> ->**DELETE** http://example.org:9090/plugins/restapi/v1/lockouts/testuser +Changes a roster entry on the roster (buddies / contact list) of a particular user. -## Retrieve user roster -Endpoint to get roster entries (buddies) from a specific user ->**GET** /users/{username}/roster +**Parameters** -**Payload:** none +| Name | Located in | Required | Description | Default value | +|------|------------|----------|-------------|---------------| +| username | path | yes | The username of the user for which to update a roster entry. | | +| rosterJid | path | yes | The JID of the entry/contact to update. | | -**Return value:** Roster +**Request body** (required): `RosterItemEntity` (XML or JSON) - The updated definition of the roster entry. -### Possible parameters +**Responses** -| Parameter | Parameter Type | Description | Default value | -|-----------|-----------------|----------------|---------------| -| username | @Path | Exact username | | +| Status | Description | Response body | +|--------|-------------|---------------| +| 200 | The roster entry was updated. | | +| 400 | A roster entry cannot be added with a 'shared group'. | `ErrorResponse` | +| 404 | No user with this username exists. | `ErrorResponse` | +| 409 | A roster entry already exists for the provided contact JID. | `ErrorResponse` | -### Examples ->**Header:** Authorization: Basic YWRtaW46MTIzNDU= -> ->**GET** http://example.org:9090/plugins/restapi/v1/users/testuser/roster +## Remove roster entry -## Create a user roster entry -Endpoint to add a new roster entry to a user ->**POST** /users/{username}/roster +> **DELETE** /plugins/restapi/v1/users/{username}/roster/{rosterJid} -**Payload:** RosterItem +Removes one of the roster entries (contacts) of a particular user. -**Return value:** HTTP status 201 (Created) +**Parameters** -### Possible parameters +| Name | Located in | Required | Description | Default value | +|------|------------|----------|-------------|---------------| +| username | path | yes | The username of the user for which to remove a roster entry. | | +| rosterJid | path | yes | The JID of the entry/contact to remove. | | -| Parameter | Parameter Type | Description | Default value | -|-----------|-----------------|----------------|---------------| -| username | @Path | Exact username | | +**Responses** -### Examples ->**Header:** Authorization: Basic YWRtaW46MTIzNDU= -> ->**Header:** Content-Type application/xml -> ->**POST** http://example.org:9090/plugins/restapi/v1/users/testuser/roster - -**Payload:** -Payload Example 1 (required parameters): -```xml - - - peter@pan.de - -``` -Payload Example 2 (available parameters): -```xml - - - peter@pan1.de - Peter1 - 3 - - Friends - - -``` - -## Delete a user roster entry -Endpoint to remove a roster entry from a user ->**DELETE** /users/{username}/roster/{jid} - -**Payload:** none - -**Return value:** HTTP status 200 (OK) - -### Possible parameters - -| Parameter | Parameter Type | Description | Default value | -|-----------|-----------------|------------------------|---------------| -| username | @Path | Exact username | | -| jid | @Path | JID of the roster item | | - -### Examples ->**Header:** Authorization: Basic YWRtaW46MTIzNDU= -> ->**DELETE** http://example.org:9090/plugins/restapi/v1/users/testuser/roster/peter@pan.de +| Status | Description | Response body | +|--------|-------------|---------------| +| 200 | The entry was removed from the roster. | | +| 400 | A roster entry cannot be removed from a 'shared group'. | `ErrorResponse` | +| 404 | No user with this username exists, or its roster did not contain this entry. | `ErrorResponse` | -## Update a user roster entry -Endpoint to update a roster entry ->**PUT** /users/{username}/roster/{jid} +## Get user's vCard -**Payload:** RosterItem +> **GET** /plugins/restapi/v1/users/{username}/vcard -**Return value:** HTTP status 200 (OK) +Retrieves the vCard for a particular user. -### Possible parameters +**Parameters** -| Parameter | Parameter Type | Description | Default value | -|-----------|-----------------|------------------------|---------------| -| username | @Path | Exact username | | -| jid | @Path | JID of the roster item | | +| Name | Located in | Required | Description | Default value | +|------|------------|----------|-------------|---------------| +| username | path | yes | The username of the user for which to return the vCard. | | -### Examples ->**Header:** Authorization: Basic YWRtaW46MTIzNDU= -> ->**Header:** Content-Type application/xml -> ->**PUT** http://example.org:9090/plugins/restapi/v1/users/testuser/roster/peter@pan.de - -**Payload:** -```xml - - - peter@pan.de - Peter Pan - 0 - - Support - - -``` - -## Retrieve user's vcard -Endpoint to get the vCard of a particular user -> **GET** /users/{username}/vcard - -**Payload:** none - -**Return value:** vCard XML data - -### Possible parameters - -| Parameter | Parameter Type | Description | Default value | -|-----------|-----------------|----------------|---------------| -| username | @Path | Exact username | | - -### Examples ->**Header:** Authorization: Basic YWRtaW46MTIzNDU= -> ->**GET** http://example.org:9090/plugins/restapi/v1/users/testuser/vcard +**Responses** -## Add or update user's vCard -Endpoint to add or replace a vCard of a particular user. -> **PUT** /users/{username}/vcard +| Status | Description | Response body | +|--------|-------------|---------------| +| 200 | The vCard of the user. | | +| 204 | No vCard found. | | -**Payload:** vCard XML data +## Update vCard -**Return value:** HTTP status 200 (Created) +> **PUT** /plugins/restapi/v1/users/{username}/vcard -### Possible parameters +Creates or changes a vCard of a particular user. +**Parameters** -| Parameter | Parameter Type | Description | Default value | -|-----------|-----------------|----------------|---------------| -| username | @Path | Exact username | | +| Name | Located in | Required | Description | Default value | +|------|------------|----------|-------------|---------------| +| username | path | yes | The username of the user for which to update the vCard. | | -### Examples ->**Header:** Authorization: Basic YWRtaW46MTIzNDU= -> ->**Header:** Content-Type application/xml -> ->**POST** http://example.org:9090/plugins/restapi/v1/users/testuser/vcard - -**Payload:** -```xml - - - - Doe - Janice - Francis - - - - - - Jane - Janice Francis Doe - - <URL/> - <EMAIL> - <HOME/> - <INTERNET/> - <PREF/> - <USERID>j.doe@example.org</USERID> - </EMAIL> - <TEL> - <WORK/> - <VOICE/> - <NUMBER/> - </TEL> - <TEL> - <WORK/> - <PAGER/> - <NUMBER/> - </TEL> - <TEL> - <WORK/> - <FAX/> - <NUMBER/> - </TEL> - <TEL> - <WORK/> - <CELL/> - <NUMBER/> - </TEL> - <TEL> - <HOME/> - <VOICE/> - <NUMBER/> - </TEL> - <TEL> - <HOME/> - <PAGER/> - <NUMBER/> - </TEL> - <TEL> - <HOME/> - <FAX/> - <NUMBER/> - </TEL> - <TEL> - <HOME/> - <CELL/> - <NUMBER/> - </TEL> - <ADR> - <WORK/> - <LOCALITY/> - <CTRY/> - <STREET/> - <PCODE/> - <REGION/> - </ADR> - <ADR> - <HOME/> - <LOCALITY/> - <CTRY/> - <STREET/> - <PCODE/> - <REGION/> - </ADR> -</vCard> -``` - -## Delete user's vcard -Endpoint to remove the vCard of a particular user -> **DELETE** /users/{username}/vcard - -**Payload:** none - -**Return value:** none - -### Possible parameters - -| Parameter | Parameter Type | Description | Default value | -|-----------|-----------------|----------------|---------------| -| username | @Path | Exact username | | - -### Examples ->**Header:** Authorization: Basic YWRtaW46MTIzNDU= -> ->**DELETE** http://example.org:9090/plugins/restapi/v1/users/testuser/vcard +**Request body** (required): string (XML) - The updated definition of the vCard. -# Chat room related REST Endpoints +**Responses** -## Retrieve all chat services +| Status | Description | Response body | +|--------|-------------|---------------| +| 200 | The vCard was updated/created. | | +| 400 | Provided data could not be parsed. | `ErrorResponse` | +| 409 | Cannot change vCard, as Openfire is configured to have read-only vCards. | `ErrorResponse` | -Endpoint to get all chat services ->**GET** /chatservices +## Delete vCard -**Payload:** none +> **DELETE** /plugins/restapi/v1/users/{username}/vcard -**Return value:** Chat services +Removes a vCard of a particular user. -**Possible parameters:** none +**Parameters** -### Examples +| Name | Located in | Required | Description | Default value | +|------|------------|----------|-------------|---------------| +| username | path | yes | The username of the user for which to delete the vCard. | | ->**Header**: Authorization: Basic YWRtaW46MTIzNDU= -> ->**GET** http://example.org:9090/plugins/restapi/v1/chatservices +**Responses** -## Create a chat service -Endpoint to create a new chat service. ->**POST** /chatservices +| Status | Description | Response body | +|--------|-------------|---------------| +| 200 | The vCard was deleted. | | +| 409 | Cannot delete vCard, as Openfire is configured to have read-only vCards. | `ErrorResponse` | -**Payload:** Chatservice +## Lock user out -**Return value:** HTTP status 201 (Created) +> **POST** /plugins/restapi/v1/lockouts/{username} -**Possible parameters:** none +Lockout / ban the user from the chat server. The user will be kicked if the user is online. -### XML Examples +**Parameters** ->**Header:** Authorization: Basic YWRtaW46MTIzNDU= -> ->**Header:** Content-Type: application/xml -> ->**POST** http://example.org:9090/plugins/restapi/v1/chatservices - -**Payload Example (available parameters):** -```xml -<?xml version="1.0" encoding="UTF-8" standalone="yes"?> -<chatService> - <serviceName>new-chat-service-name</serviceName> - <description>A mightily fine service</description> - <hidden>false</hidden> -</chatService> -``` - -### JSON Examples +| Name | Located in | Required | Description | Default value | +|------|------------|----------|-------------|---------------| +| username | path | yes | The username of the user that is to be locked out. | | ->**Header:** Authorization: Basic YWRtaW46MTIzNDU= -> ->**Header:** Content-Type: application/json -> ->**POST** http://example.org:9090/plugins/restapi/v1/chatservices +**Responses** -**Payload Example (available parameters):** -```json -{ - "serviceName": "new-chat-service-name", - "description": "A mightily fine service", - "hidden": false -} -``` +| Status | Description | Response body | +|--------|-------------|---------------| +| 201 | The user was locked out. | | +| 404 | No user with this username exists. | `ErrorResponse` | -## Retrieve all chat rooms -Endpoint to get all chat rooms ->**GET** /chatrooms +## Unlock user -**Payload:** none +> **DELETE** /plugins/restapi/v1/lockouts/{username} -**Return value:** Chatrooms +Removes a previously applied lockout / ban of a user. -### Possible parameters +**Parameters** -| Parameter | Parameter Type | Description | Default value | -|--------------|----------------|-------------------------------------------------------------------------------|---------------| -| servicename | @QueryParam | The name of the Group Chat Service | conference | -| type | @QueryParam | **public:** Only as List Room in Directory set rooms <br> **all:** All rooms. | public | -| search | @QueryParam | Search/Filter by room name. <br> This act like the wildcard search %String% | | +| Name | Located in | Required | Description | Default value | +|------|------------|----------|-------------|---------------| +| username | path | yes | The username of the user for which the lockout is to be undone. | | -### Examples +**Responses** ->**Header**: Authorization: Basic YWRtaW46MTIzNDU= -> ->**GET** http://example.org:9090/plugins/restapi/v1/chatrooms -> ->**GET** http://example.org:9090/plugins/restapi/v1/chatrooms?type=all -> ->**GET** http://example.org:9090/plugins/restapi/v1/chatrooms?type=all&servicename=privateconf -> ->**GET** http://example.org:9090/plugins/restapi/v1/chatrooms?search=test +| Status | Description | Response body | +|--------|-------------|---------------| +| 200 | User is unlocked. | | +| 404 | No user with this username exists. | `ErrorResponse` | -## Retrieve a chat room -Endpoint to get information over specific chat room ->**GET** /chatrooms<span>/{roomName} +# User Group -**Payload:** none +Managing Openfire user groups. -**Return value:** Chatroom +## Get groups -### Possible parameters +> **GET** /plugins/restapi/v1/groups -| Parameter | Parameter Type | Description | Default value | -|-------------|----------------|------------------------------------|---------------| -| roomname | @Path | Exact room name | | -| servicename | @QueryParam | The name of the Group Chat Service | conference | +Get a list of all user groups. -### Examples +**Responses** ->**Header:** Authorization: Basic YWRtaW46MTIzNDU= -> ->**GET** http://example.org:9090/plugins/restapi/v1/chatrooms/test -> ->**GET** http://example.org:9090/plugins/restapi/v1/chatrooms/test?servicename=privateconf +| Status | Description | Response body | +|--------|-------------|---------------| +| 200 | All groups. | `GroupEntities` (XML or JSON) | -## Retrieve chat room participants -Endpoint to get all participants with a role of specified room. ->**GET** /chatrooms/{roomName}/participants +## Create group -**Payload:** none +> **POST** /plugins/restapi/v1/groups -**Return value:** Participants +Create a new user group. -### Possible parameters +**Request body** (required): `GroupEntity` (XML or JSON) - The group that needs to be created. -| Parameter | Parameter Type | Description | Default value | -|-------------|-----------------|------------------------------------|---------------| -| roomname | @Path | Exact room name | | -| servicename | @QueryParam | The name of the Group Chat Service | conference | +**Responses** -### Examples +| Status | Description | Response body | +|--------|-------------|---------------| +| 201 | Group created. | | +| 400 | Group or group name missing, or invalid syntax for a property. | `ErrorResponse` | +| 409 | Group already exists. | `ErrorResponse` | ->**Header:** Authorization: Basic YWRtaW46MTIzNDU= -> ->**GET** http://example.org:9090/plugins/restapi/v1/chatrooms/room1/participants +## Get group -## Retrieve chat room occupants -Endpoint to get all occupants (all roles / affiliations) of a specified room. ->**GET** /chatrooms/{roomName}/occupants +> **GET** /plugins/restapi/v1/groups/{groupName} -**Payload:** none +Get one specific user group by name. -**Return value:** Occupants +**Parameters** -### Possible parameters +| Name | Located in | Required | Description | Default value | +|------|------------|----------|-------------|---------------| +| groupName | path | yes | The name of the group that needs to be fetched. Example: `Colleagues` | | -| Parameter | Parameter Type | Description | Default value | -|-------------|-----------------|------------------------------------|---------------| -| roomname | @Path | Exact room name | | -| servicename | @QueryParam | The name of the Group Chat Service | conference | +**Responses** -### Examples +| Status | Description | Response body | +|--------|-------------|---------------| +| 200 | The group. | `GroupEntity` (XML or JSON) | +| 404 | Group with this name not found. | `ErrorResponse` (XML or JSON) | ->**Header:** Authorization: Basic YWRtaW46MTIzNDU= -> ->**GET** http://example.org:9090/plugins/restapi/v1/chatrooms/room1/occupants +## Update group -## Retrieve chat room message history -Endpoint to get the chat message history of a specified room. +> **PUT** /plugins/restapi/v1/groups/{groupName} ->**GET** /chatrooms/{roomName}/chathistory +Updates / overwrites an existing user group. Note that the name of the group cannot be changed. -**Payload:** none +**Parameters** -**Return value:** Chat History +| Name | Located in | Required | Description | Default value | +|------|------------|----------|-------------|---------------| +| groupName | path | yes | The name of the group that needs to be updated. Example: `Colleagues` | | -### Possible parameters +**Request body** (required): `GroupEntity` (XML or JSON) - The new group definition that needs to overwrite the old definition. -| Parameter | Parameter Type | Description | Default value | -|-------------|-----------------|------------------------------------|---------------| -| roomname | @Path | Exact room name | | -| servicename | @QueryParam | The name of the Group Chat Service | conference | +**Responses** -## Create a chat room -Endpoint to create a new chat room. ->**POST** /chatrooms +| Status | Description | Response body | +|--------|-------------|---------------| +| 200 | Group updated. | | +| 400 | Group or group name missing, or name does not match existing group, or invalid syntax for a property. | `ErrorResponse` | +| 404 | Group with this name not found. | `ErrorResponse` | -**Payload:** Chatroom +## Delete group -**Return value:** HTTP status 201 (Created) +> **DELETE** /plugins/restapi/v1/groups/{groupName} -### Possible parameters +Removes an existing user group. -| Parameter | Parameter Type | Description | Default value | -|-----------------|-----------------|-------------------------------------------------|---------------| -| servicename | @QueryParam | The name of the Group Chat Service | conference | -| sendInvitations | @QueryParam | Whether to send invitations to affiliated users | false | +**Parameters** -### XML Examples +| Name | Located in | Required | Description | Default value | +|------|------------|----------|-------------|---------------| +| groupName | path | yes | The name of the group that needs to be removed. Example: `Colleagues` | | ->**Header:** Authorization: Basic YWRtaW46MTIzNDU= -> ->**Header:** Content-Type: application/xml -> ->**POST** http://example.org:9090/plugins/restapi/v1/chatrooms - -**Payload Example 1 (required parameters):** -```xml -<?xml version="1.0" encoding="UTF-8" standalone="yes"?> -<chatRoom> - <naturalName>global-1</naturalName> - <roomName>global</roomName> - <description>Global Chat Room</description> -</chatRoom> -``` - -**Payload Example 2 (available parameters):** -```xml -<?xml version="1.0" encoding="UTF-8" standalone="yes"?> -<chatRoom> - <roomName>global</roomName> - <naturalName>global-2</naturalName> - <description>Global Chat Room</description> - <subject>global-2 Subject</subject> - <creationDate>2014-02-12T15:52:37.592+01:00</creationDate> - <modificationDate>2014-09-12T15:35:54.702+02:00</modificationDate> - <maxUsers>0</maxUsers> - <persistent>true</persistent> - <publicRoom>true</publicRoom> - <registrationEnabled>false</registrationEnabled> - <canAnyoneDiscoverJID>false</canAnyoneDiscoverJID> - <canOccupantsChangeSubject>false</canOccupantsChangeSubject> - <canOccupantsInvite>false</canOccupantsInvite> - <canChangeNickname>false</canChangeNickname> - <logEnabled>true</logEnabled> - <loginRestrictedToNickname>false</loginRestrictedToNickname> - <membersOnly>false</membersOnly> - <moderated>false</moderated> - <allowPM>anyone</allowPM> - <broadcastPresenceRoles> - <broadcastPresenceRole>moderator</broadcastPresenceRole> - <broadcastPresenceRole>participant</broadcastPresenceRole> - <broadcastPresenceRole>visitor</broadcastPresenceRole> - </broadcastPresenceRoles> - <owners> - <owner>owner@localhost</owner> - </owners> - <admins> - <admin>admin@localhost</admin> - </admins> - <members> - <member>member2@localhost</member> - <member>member1@localhost</member> - </members> - <outcasts> - <outcast>outcast1@localhost</outcast> - </outcasts> -</chatRoom> -``` - -### JSON Examples +**Responses** ->**Header:** Authorization: Basic YWRtaW46MTIzNDU= -> ->**Header:** Content-Type: application/json -> ->**POST** http://example.org:9090/plugins/restapi/v1/chatrooms - -**Payload Example 1 (required parameters):** -```json -{ - "roomName": "global", - "naturalName": "global-2", - "description": "Global chat room" -} -``` - -**Payload Example 2 (available parameters):** -```json -{ - "roomName": "global-1", - "naturalName": "global-1_test_hello", - "description": "Global chat room", - "subject": "Global chat room subject", - "creationDate": "2012-10-18T16:55:12.803+02:00", - "modificationDate": "2014-07-10T09:49:12.411+02:00", - "maxUsers": "0", - "persistent": "true", - "publicRoom": "true", - "registrationEnabled": "false", - "canAnyoneDiscoverJID": "true", - "canOccupantsChangeSubject": "false", - "canOccupantsInvite": "false", - "canChangeNickname": "false", - "logEnabled": "true", - "loginRestrictedToNickname": "true", - "membersOnly": "false", - "moderated": "false", - "allowPM": "anyone", - "broadcastPresenceRoles": { - "broadcastPresenceRole": [ - "moderator", - "participant", - "visitor" - ] - }, - "owners": { - "owner": "owner@localhost" - }, - "admins": { - "admin": [ - "admin@localhost", - "admin2@localhost" - ] - }, - "members": { - "member": [ - "member@localhost", - "member2@localhost" - ] - }, - "outcasts": { - "outcast": [ - "outcast@localhost", - "outcast2@localhost" - ] - } -} -``` - -**REST API Version 1.3.0 and later - Payload Example 2 (available parameters):** -```json -{ - "roomName": "global-1", - "naturalName": "global-1_test_hello", - "description": "Global chat room", - "subject": "Global chat room subject", - "creationDate": "2012-10-18T16:55:12.803+02:00", - "modificationDate": "2014-07-10T09:49:12.411+02:00", - "maxUsers": "0", - "persistent": "true", - "publicRoom": "true", - "registrationEnabled": "false", - "canAnyoneDiscoverJID": "true", - "canOccupantsChangeSubject": "false", - "canOccupantsInvite": "false", - "canChangeNickname": "false", - "logEnabled": "true", - "loginRestrictedToNickname": "true", - "membersOnly": "false", - "moderated": "false", - "allowPM": "anyone", - "broadcastPresenceRoles": [ - "moderator", - "participant", - "visitor" - ], - "owners": [ - "owner@localhost" - ], - "admins": [ - "admin@localhost" - ], - "members": [ - "member@localhost" - ], - "outcasts": [ - "outcast@localhost" - ] -} -``` - - - - - - - -## Create multiple chat room -Endpoint to create multiple new chat rooms at once. ->**POST** /chatrooms/bulk - -**Payload:** Chatrooms - -**Return value:** Result list, ordered by successes and failures -```xml -<?xml version="1.0" encoding="UTF-8" standalone="yes"?> -<results> - <success> - <result> - <roomName>room1</roomName> - <resultType>Success</resultType> - <message>Room was successfully created</message> - </result> - <result> - <roomName>room2</roomName> - <resultType>Success</resultType> - <message>Room was successfully created</message> - </result> - </success> - <failure/> - <other/> -</results> -``` - -```json -{ - "success": [ - { - "roomName": "room1", - "resultType": "Success", - "message": "Room was successfully created" - }, - { - "roomName": "room2", - "resultType": "Success", - "message": "Room was successfully created" - } - ], - "failure": [], - "other": [] -} -``` -### Possible parameters - -| Parameter | Parameter Type | Description | Default value | -|-----------------|-----------------|-------------------------------------------------------|---------------| -| servicename | @QueryParam | The name of the Group Chat Service | conference | -| sendInvitations | @QueryParam | Whether to send invitations to newly affiliated users | false | - -### XML Examples +| Status | Description | Response body | +|--------|-------------|---------------| +| 200 | Group deleted. | | +| 404 | Group with this name not found. | `ErrorResponse` | ->**Header:** Authorization: Basic YWRtaW46MTIzNDU= -> ->**Header:** Content-Type: application/xml -> ->**POST** http://example.org:9090/plugins/restapi/v1/chatrooms/bulk - -**Payload Example:** -```xml -<?xml version="1.0" encoding="UTF-8" standalone="yes"?> -<chatRooms> - <chatRoom> - <roomName>room1</roomName> - <description>description1</description> - </chatRoom> - <chatRoom> - <roomName>room2</roomName> - <description>description1</description> - </chatRoom> -</chatRooms> -``` - -For more examples, with more parameters, see the [create a chat room](#create-a-chat-room) endpoint. - -### JSON Examples +# Chat service ->**Header:** Authorization: Basic YWRtaW46MTIzNDU= -> ->**Header:** Content-Type: application/json -> ->**POST** http://example.org:9090/plugins/restapi/v1/chatrooms +Managing multi-user chat services. -**Payload Example 1 (required parameters):** -```json -{ - "chatRooms": [ - { "roomName": "room1", "description": "description1" }, - { "roomName": "room2", "description": "description2" } - ] -} -``` +## Get chat services -For more examples, with more parameters, see the [create a chat room](#create-a-chat-room) endpoint. +> **GET** /plugins/restapi/v1/chatservices +Get a list of all multi-user chat services. +**Responses** +| Status | Description | Response body | +|--------|-------------|---------------| +| 200 | All chat services. | `MUCServiceEntities` (XML or JSON) | +## Create chat service +> **POST** /plugins/restapi/v1/chatservices +Create a new multi-user chat service. +**Request body** (required): `MUCServiceEntity` (XML or JSON) - The MUC service that needs to be created. +**Responses** -## Delete a chat room -Endpoint to delete a chat room. ->**DELETE** /chatrooms/{roomName} +| Status | Description | Response body | +|--------|-------------|---------------| +| 201 | Service created. | | +| 403 | Service creation is not permitted. | `ErrorResponse` | +| 409 | Service already exists, or another conflict occurred while creating the service. | `ErrorResponse` | -**Payload:** none +# Chat room -**Return value:** HTTP status 200 (OK) +Managing multi-user chat rooms. -### Possible parameters +## Get chat rooms -| Parameter | Parameter Type | Description | Default value | -|-------------|-----------------|------------------------------------|---------------| -| roomname | @Path | Exact room name | | -| servicename | @QueryParam | The name of the Group Chat Service | conference | +> **GET** /plugins/restapi/v1/chatrooms -### Examples +Get a list of all multi-user chat rooms of a particular chat room service. ->**Header:** Authorization: Basic YWRtaW46MTIzNDU= -> ->**DELETE** http://example.org:9090/plugins/restapi/v1/chatrooms/testroom -> ->**DELETE** http://example.org:9090/plugins/restapi/v1/chatrooms/testroom?servicename=privateconf +**Parameters** -## Update a chat room -Endpoint to update a chat room. ->**PUT** /chatrooms/{roomName} +| Name | Located in | Required | Description | Default value | +|------|------------|----------|-------------|---------------| +| servicename | query | no | The name of the MUC service for which to return all chat rooms. Example: `conference` | `conference` | +| type | query | no | Room type-based filter: 'all' or 'public'. | `public` | +| search | query | no | Search/Filter by room name.<br>This acts like the wildcard search %String% Example: `conference` | | +| expandGroups | query | no | For all groups defined in owners, admins, members and outcasts, list individual members instead of the group name. | `false` | -**Payload:** Chatroom +**Responses** -**Return value:** HTTP status 200 (OK) +| Status | Description | Response body | +|--------|-------------|---------------| +| 200 | All chat rooms. | `MUCRoomEntities` (XML or JSON) | +| 404 | MUC service does not exist or is not accessible. | `ErrorResponse` (XML or JSON) | -### Possible parameters +## Create chat room -| Parameter | Parameter Type | Description | Default value | -|-----------------|----------------|-------------------------------------------------------|---------------| -| roomname | @Path | Exact room name | | -| servicename | @QueryParam | The name of the Group Chat Service | conference | -| sendInvitations | @QueryParam | Whether to send invitations to newly affiliated users | false | +> **POST** /plugins/restapi/v1/chatrooms -### Examples ->**Header:** Authorization: Basic YWRtaW46MTIzNDU= -> ->**Header:** Content-Type application/xml -> ->**PUT** http://example.org:9090/plugins/restapi/v1/chatrooms/global - -**Payload:** -```xml -<?xml version="1.0" encoding="UTF-8" standalone="yes"?> -<chatRoom> - <roomName>global</roomName> - <naturalName>global-2</naturalName> - <description>Global Chat Room edit</description> - <subject>New subject</subject> - <password>test</password> - <creationDate>2014-02-12T15:52:37.592+01:00</creationDate> - <modificationDate>2014-09-12T14:20:56.286+02:00</modificationDate> - <maxUsers>0</maxUsers> - <persistent>true</persistent> - <publicRoom>true</publicRoom> - <registrationEnabled>false</registrationEnabled> - <canAnyoneDiscoverJID>false</canAnyoneDiscoverJID> - <canOccupantsChangeSubject>false</canOccupantsChangeSubject> - <canOccupantsInvite>false</canOccupantsInvite> - <canChangeNickname>false</canChangeNickname> - <logEnabled>true</logEnabled> - <loginRestrictedToNickname>false</loginRestrictedToNickname> - <membersOnly>false</membersOnly> - <moderated>false</moderated> - <allowPM>anyone</allowPM> - <broadcastPresenceRoles/> - <owners> - <owner>owner@localhost</owner> - </owners> - <admins> - <admin>admin@localhost</admin> - </admins> - <members> - <member>member2@localhost</member> - <member>member1@localhost</member> - </members> - <outcasts> - <outcast>outcast1@localhost</outcast> - </outcasts> -</chatRoom> -``` - -## Invite user or user group to a chat Room - -Endpoint to invite a user or a user group to a room. -> **Header:** Authorization: Basic YWRtaW46MTIzNDU= -> -> **Header:** Content-Type: application/xml -> -> **POST** http://localhost:9090/plugins/restapi/v1/chatrooms/{roomName}/invite/{name} - -**Payload Example:** - -```xml -<?xml version="1.0" encoding="UTF-8" standalone="yes"?> -<mucInvitation> - <reason>Hello, come to this room, it is nice</reason> -</mucInvitation> -``` -**Return value:** HTTP status 200 (OK) - -### Possible parameters -| Parameter | Parameter Type | Description | Default value | -|-----------|-----------------|---------------------------------------------------------------|---------------| -| roomname | @Path | Exact room name | | -| name | @Path | The local username or group name or the user JID or group JID | | - -## Invite multiple users and/or user groups to a chat Room - -Endpoint to invite multiple users and/or user groups to a room. Works both with JIDs and (user/group) names. -> **Header:** Authorization: Basic YWRtaW46MTIzNDU= -> -> **Header:** Content-Type: application/xml -> -> **POST** http://localhost:9090/plugins/restapi/v1/chatrooms/{roomName}/invite - -**Payload Example:** - -```xml -<?xml version="1.0" encoding="UTF-8" standalone="yes"?> -<mucInvitation> - <reason>Hello, come to this room, it is nice</reason> - <jidsToInvite> - <jid>jane@example.org</jid> - <jid>ADNMQP8=@example.org/695c6ae413c00446733d926ccadefd8b</jid> - <jid>john</jid> - <jid>SomeGroupName</jid> - </jidsToInvite> -</mucInvitation> -``` -**Return value:** HTTP status 200 (OK) - -### Possible parameters -| Parameter | Parameter Type | Description | Default value | -|-----------|-----------------|---------------------------------------------------------------|---------------| -| roomname | @Path | Exact room name | | - -## Get all users with a particular affiliation in a chat room -Retrieves a list of JIDs for all users with the specified affiliation in a multi-user chat room. - ->**GET** /chatrooms/{roomName}/{affiliation} - -**Payload:** none - -**Return value:** HTTP status 200 (OK) - -### Possible parameters - -| Parameter | Parameter Type | Description | Default value | -|-------------|-----------------|--------------------------------------------------------------------------------------------|---------------| -| roomname | @Path | Exact room name | | -| affiliation | @Path | Available affiliations: <br>**owners** <br> **admins** <br> **members** <br> **outcasts** | | -| servicename | @QueryParam | The name of the Group Chat Service | conference | - -### Examples ->**Header:** Authorization: Basic YWRtaW46MTIzNDU= -> ->**Header:** Content-Type application/xml -> ->**GET** http://example.org:9090/plugins/restapi/v1/chatrooms/global/member - -**Return payload:** -```xml -<?xml version="1.0" encoding="UTF-8" standalone="yes"?> -<members> - <member>member2@localhost</member> - <member>member1@localhost</member> -</members> -``` - -## Add user with affiliation to chat room -Endpoint to add a new user with affiliation to a room. ->**POST** /chatrooms/{roomName}/{affiliation}/{name} - -**Payload:** none - -**Return value:** HTTP status 201 (Created) - -### Possible parameters - -| Parameter | Parameter Type | Description | Default value | -|-----------------|-----------------|--------------------------------------------------------------------------------------------|---------------| -| roomname | @Path | Exact room name | | -| name | @Path | The local username or the user JID | | -| affiliation | @Path | Available affiliations: <br>**owners** <br> **admins** <br> **members** <br> **outcasts** | | -| servicename | @QueryParam | The name of the Group Chat Service | conference | -| sendInvitations | @QueryParam | Whether to send invitation to the newly affiliated user | false | - -### Examples ->**Header:** Authorization: Basic YWRtaW46MTIzNDU= -> ->**Header:** Content-Type application/xml -> ->**POST** http://example.org:9090/plugins/restapi/v1/chatrooms/global/owners/testUser -> ->**POST** http://example.org:9090/plugins/restapi/v1/chatrooms/global/owners/testUser@openfire.com -> ->**POST** http://example.org:9090/plugins/restapi/v1/chatrooms/global/admins/testUser -> ->**POST** http://example.org:9090/plugins/restapi/v1/chatrooms/global/members/testUser?sendInvitations=true -> ->**POST** http://example.org:9090/plugins/restapi/v1/chatrooms/global/outcasts/testUser -> ->**POST** http://example.org:9090/plugins/restapi/v1/chatrooms/global/owners/testUser?servicename=privateconf - -## Replace all users with a affiliation in a chat room -Endpoint to replace all users with a particular affiliation in a multi-user chat room. Note that a user can only have one type of affiliation with a room. By adding a user using a particular affiliation, any other pre-existing affiliation is removed. ->**PUT** /chatrooms/{roomName}/{affiliation} - -**Payload:** list of affiliations - -**Return value:** HTTP status 201 (Created) - -### Possible parameters - -| Parameter | Parameter Type | Description | Default value | -|-----------------|-----------------|--------------------------------------------------------------------------------------------|---------------| -| roomname | @Path | Exact room name | | -| affiliation | @Path | Available affiliations: <br>**owners** <br> **admins** <br> **members** <br> **outcasts** | | -| servicename | @QueryParam | The name of the Group Chat Service | conference | -| sendInvitations | @QueryParam | Whether to send invitation to newly affiliated users | false | - -### Examples ->**Header:** Authorization: Basic YWRtaW46MTIzNDU= -> ->**Header:** Content-Type application/xml -> ->**PUT** http://example.org:9090/plugins/restapi/v1/chatrooms/global/members -> -**Request Payload:** -```xml -<?xml version="1.0" encoding="UTF-8" standalone="yes"?> -<members> - <member>member2@localhost</member> - <member>member1@localhost</member> -</members> -``` - -## Add multiple users with a affiliation to a chat room -Endpoint to add multiple users with an affiliation to a multi-user chat room. Note that a user can only have one type of affiliation with a room. By adding a user using a particular affiliation, any other pre-existing affiliation is removed. ->**PUT** /chatrooms/{roomName}/{affiliation} - -**Payload:** list of affiliations - -**Return value:** HTTP status 201 (Created) - -### Possible parameters - -| Parameter | Parameter Type | Description | Default value | -|-----------------|-----------------|-------------------------------------------------------------------------------------------|---------------| -| roomname | @Path | Exact room name | | -| affiliation | @Path | Available affiliation: <br>**owners** <br> **admins** <br> **members** <br> **outcasts** | | -| servicename | @QueryParam | The name of the Group Chat Service | conference | -| sendInvitations | @QueryParam | Whether to send invitations to newly affiliated users | false | - -### Examples ->**Header:** Authorization: Basic YWRtaW46MTIzNDU= -> ->**Header:** Content-Type application/xml -> ->**POST** http://example.org:9090/plugins/restapi/v1/chatrooms/global/members -> -**Request Payload:** -```xml -<?xml version="1.0" encoding="UTF-8" standalone="yes"?> -<members> - <member>member2@localhost</member> - <member>member1@localhost</member> -</members> -``` - -## Add group with affiliation to chat room -Endpoint to add a new group with affiliation to a room. ->**POST** /chatrooms/{roomName}/{affiliation}/group/{name} - -**Payload:** none - -**Return value:** HTTP status 201 (Created) - -### Possible parameters - -| Parameter | Parameter Type | Description | Default value | -|-----------------|----------------|--------------------------------------------------------------------------------------------|---------------| -| roomname | @Path | Exact room name | | -| name | @Path | The group name | | -| affiliation | @Path | Available affiliations: <br>**owners** <br> **admins** <br> **members** <br> **outcasts** | | -| servicename | @QueryParam | The name of the Group Chat Service | conference | -| sendInvitations | @QueryParam | Whether to send invitations to the users in the newly affiliated groups | false | - -### Examples ->**Header:** Authorization: Basic YWRtaW46MTIzNDU= -> ->**Header:** Content-Type application/xml -> ->**POST** http://example.org:9090/plugins/restapi/v1/chatrooms/global/owners/group/testGroup -> ->**POST** http://example.org:9090/plugins/restapi/v1/chatrooms/global/admins/group/testGroup -> ->**POST** http://example.org:9090/plugins/restapi/v1/chatrooms/global/members/group/testGroup -> ->**POST** http://example.org:9090/plugins/restapi/v1/chatrooms/global/outcasts/group/testGroup -> ->**POST** http://example.org:9090/plugins/restapi/v1/chatrooms/global/owners/group/testUser?servicename=privateconf - - -## Delete a user from a chat room -Endpoint to remove a room user affiliation. ->**DELETE** /chatrooms/{roomName}/{affiliations}/{name} - -**Payload:** none - -**Return value:** HTTP status 200 (OK) - -### Possible parameters - -| Parameter | Parameter Type | Description | Default value | -|--------------|-----------------|--------------------------------------------------------------------------------------------|---------------| -| roomname | @Path | Exact room name | | -| name | @Path | The local username or the user JID | | -| affiliations | @Path | Available affiliations: <br>**owners** <br> **admins** <br> **members** <br> **outcasts** | | -| servicename | @QueryParam | The name of the Group Chat Service | conference | - -### Examples ->**Header:** Authorization: Basic YWRtaW46MTIzNDU= -> ->**Header:** Content-Type application/xml -> ->**DELETE** http://example.org:9090/plugins/restapi/v1/chatrooms/global/owners/testUser -> ->**DELETE** http://example.org:9090/plugins/restapi/v1/chatrooms/global/owners/testUser@openfire.com -> ->**DELETE** http://example.org:9090/plugins/restapi/v1/chatrooms/global/admins/testUser -> ->**DELETE** http://example.org:9090/plugins/restapi/v1/chatrooms/global/members/testUser -> ->**DELETE** http://example.org:9090/plugins/restapi/v1/chatrooms/global/outcasts/testUser -> ->**DELETE** http://example.org:9090/plugins/restapi/v1/chatrooms/global/owners/testUser?servicename=privateconf +Create a new multi-user chat room. -# System related REST Endpoints +**Parameters** -## Retrieve all system properties -Endpoint to get all system properties ->**GET** /system/properties +| Name | Located in | Required | Description | Default value | +|------|------------|----------|-------------|---------------| +| servicename | query | no | The name of the MUC service in which to create a chat room. Example: `conference` | `conference` | +| sendInvitations | query | no | Whether to send invitations to newly affiliated users. Example: `true` | `false` | -**Payload:** none +**Request body** (required): `MUCRoomEntity` (XML or JSON) - The MUC room that needs to be created. -**Return value:** System properties - -### Examples +**Responses** ->**Header**: Authorization: Basic YWRtaW46MTIzNDU= -> ->**GET** http://example.org:9090/plugins/restapi/v1/system/properties +| Status | Description | Response body | +|--------|-------------|---------------| +| 201 | Room created. | | +| 403 | Room creation is not permitted. | `ErrorResponse` | +| 404 | MUC service does not exist or is not accessible. | `ErrorResponse` | +| 409 | Room already exists, or another conflict occurred while creating the room. | `ErrorResponse` | -## Retrieve system property -Endpoint to get information over specific system property ->**GET** /system/properties/{propertyName} +## Create multiple chat rooms -**Payload:** none +> **POST** /plugins/restapi/v1/chatrooms/bulk -**Return value:** System property +Create a number of new multi-user chat rooms. -### Possible parameters +**Parameters** -| Parameter | Parameter Type | Description | Default value | -|--------------|-----------------|-----------------------------|---------------| -| propertyName | @Path | The name of system property | | +| Name | Located in | Required | Description | Default value | +|------|------------|----------|-------------|---------------| +| servicename | query | no | The name of the MUC service in which to create the chat rooms. Example: `conference` | `conference` | +| sendInvitations | query | no | Whether to send invitations to newly affiliated users. Example: `true` | `false` | -### Examples +**Request body** (required): `MUCRoomEntities` (XML or JSON) - The MUC rooms that need to be created. ->**Header:** Authorization: Basic YWRtaW46MTIzNDU= -> ->**GET** http://example.org:9090/plugins/restapi/v1/system/properties/xmpp.domain +**Responses** -## Create a system property -Endpoint to create a system property. Note that the name of the property must consist of one or more dot-separated parts, each consisting of ASCII letters, digits, underscores, apostrophes and hyphens. ->**POST** system/properties +| Status | Description | Response body | +|--------|-------------|---------------| +| 200 | Request has been processed. Results are reported in the response. | `RoomCreationResultEntities` (XML or JSON) | +| 404 | MUC service does not exist or is not accessible. | `ErrorResponse` (XML or JSON) | -**Payload:** System Property +## Get chat room -**Return value:** HTTP status 201 (Created) +> **GET** /plugins/restapi/v1/chatrooms/{roomName} -### Examples +Get information of a specific multi-user chat room. ->**Header:** Authorization: Basic YWRtaW46MTIzNDU= -> ->**Header:** Content-Type: application/xml -> ->**POST** http://example.org:9090/plugins/restapi/v1/system/properties +**Parameters** -**Payload Example:** -```xml -<?xml version="1.0" encoding="UTF-8" standalone="yes"?> -<property key="propertyName" value="propertyValue"/> -``` +| Name | Located in | Required | Description | Default value | +|------|------------|----------|-------------|---------------| +| roomName | path | yes | The name of the MUC room to return. Example: `lobby` | | +| servicename | query | no | The name of the MUC service for which to return a chat room. Example: `conference` | `conference` | +| expandGroups | query | no | For all groups defined in owners, admins, members and outcasts, list individual members instead of the group name. | `false` | -## Delete a system property -Endpoint to delete a system property, together with all of its child properties (properties of which the name starts with the name of this property, followed by a dot). Note that the name of the property must consist of one or more dot-separated parts, each consisting of ASCII letters, digits, underscores, apostrophes and hyphens. A deletion that could also delete other properties (for example, because the name contains an underscore, which can match any character) is rejected. ->**DELETE** /system/properties/{propertyName} +**Responses** -**Payload:** none +| Status | Description | Response body | +|--------|-------------|---------------| +| 200 | The chat room. | `MUCRoomEntity` (XML or JSON) | +| 404 | The chat room (or its service) can not be found or is not accessible. | `ErrorResponse` (XML or JSON) | -**Return value:** HTTP status 200 (OK) +## Update chat room -### Possible parameters +> **PUT** /plugins/restapi/v1/chatrooms/{roomName} -| Parameter | Parameter Type | Description | Default value | -|--------------|-----------------|-----------------------------|---------------| -| propertyName | @Path | The name of system property | | +Updates an existing multi-user chat room. -### Examples +**Parameters** ->**Header:** Authorization: Basic YWRtaW46MTIzNDU= -> ->**DELETE** http://example.org:9090/plugins/restapi/v1/system/properties/propertyName +| Name | Located in | Required | Description | Default value | +|------|------------|----------|-------------|---------------| +| roomName | path | yes | The name of the chat room that needs to be updated. Example: `lobby` | | +| servicename | query | no | The name of the MUC service in which to update a chat room. Example: `conference` | `conference` | +| sendInvitations | query | no | Whether to send invitations to newly affiliated users. Example: `true` | `false` | -## Update a system property -Endpoint to update / overwrite a system property ->**PUT** /system/properties/{propertyName} +**Request body** (required): `MUCRoomEntity` (XML or JSON) - The new MUC room definition that needs to overwrite the old definition. -**Payload:** System property +**Responses** -**Return value:** HTTP status 200 (OK) +| Status | Description | Response body | +|--------|-------------|---------------| +| 200 | Room updated. | | +| 403 | Room update/create is not permitted. | `ErrorResponse` | +| 404 | MUC service does not exist or is not accessible. | `ErrorResponse` | +| 409 | This update causes a conflict, possibly with another existing room. | `ErrorResponse` | -### Possible parameters +## Delete chat room -| Parameter | Parameter Type | Description | Default value | -|--------------|-----------------|-----------------------------|---------------| -| propertyName | @Path | The name of system property | | +> **DELETE** /plugins/restapi/v1/chatrooms/{roomName} -### Examples ->**Header:** Authorization: Basic YWRtaW46MTIzNDU= -> ->**Header:** Content-Type application/xml -> ->**PUT** http://example.org:9090/plugins/restapi/v1/system/properties/propertyName +Removes an existing multi-user chat room. -**Payload:** -```xml -<?xml version="1.0" encoding="UTF-8" standalone="yes"?> -<property key="propertyName" value="anotherValue"/> -``` +**Parameters** -## Retrieve concurrent sessions -Endpoint to get count of concurrent sessions ->**GET** /system/statistics/sessions +| Name | Located in | Required | Description | Default value | +|------|------------|----------|-------------|---------------| +| roomName | path | yes | The name of the MUC room to delete. Example: `lobby` | | +| servicename | query | no | The name of the MUC service from which to delete a chat room. Example: `conference` | `conference` | -**Payload:** none +**Responses** -**Return value:** Sessions count +| Status | Description | Response body | +|--------|-------------|---------------| +| 200 | Room deleted. | | +| 404 | The chat room (or its service) can not be found or is not accessible. | `ErrorResponse` | -### Examples +## Get room history ->**Header:** Authorization: Basic YWRtaW46MTIzNDU= -> ->**GET** http://example.org:9090/plugins/restapi/v1/system/statistics/sessions +> **GET** /plugins/restapi/v1/chatrooms/{roomName}/chathistory -## Check the 'liveness' state (using all checks) -Detects if Openfire has reached a state that it cannot recover from, except for with a restart, based on every liveness check that it has implemented. +Get messages that have been exchanged in a specific multi-user chat room. ->**GET** /system/liveness +**Parameters** -**Payload:** none +| Name | Located in | Required | Description | Default value | +|------|------------|----------|-------------|---------------| +| roomName | path | yes | The name of the chat room for which to return message history. Example: `lobby` | | +| servicename | query | no | The name of the chat room's MUC service. Example: `conference` | `conference` | -**Return value**: HTTP status 200 (OK). Any HTTP status outside the range 200-399 indicates failure. +**Responses** -## Perform 'deadlock' liveness check -Detects if Openfire has reached a state that it cannot recover from because of a deadlock. ->**GET** /system/liveness/deadlock +| Status | Description | Response body | +|--------|-------------|---------------| +| 200 | The chat room message history. | `MUCRoomMessageEntities` (XML or JSON) | +| 404 | The chat room (or its service) can not be found or is not accessible. | `ErrorResponse` (XML or JSON) | -**Payload:** none +## Invite a collection of users and/or groups -**Return value**: HTTP status 200 (OK). Any HTTP status outside the range 200-399 indicates failure. +> **POST** /plugins/restapi/v1/chatrooms/{roomName}/invite -## Perform 'properties' liveness check -Detects if Openfire has reached a state that it cannot recover from because a system property change requires a restart to take effect. ->**GET** /system/liveness/properties +Invites a collection of users and/or groups to join a specific multi-user chat room. Each entity can be identified by the JID of a user or group, or by the name of a local user or group. When a group is invited, all of its members are invited. -**Payload:** none +**Parameters** -**Return value**: HTTP status 200 (OK). Any HTTP status outside the range 200-399 indicates failure. +| Name | Located in | Required | Description | Default value | +|------|------------|----------|-------------|---------------| +| roomName | path | yes | The name of the chat room to which to invite users and/or groups. Example: `lobby` | | +| servicename | query | no | The name of the chat room's MUC service. Example: `conference` | `conference` | -## Check the 'readiness' state (using all checks) -Detects if Openfire is in a state where it is ready to process traffic, based on every readiness check that it has implemented. ->**GET** /system/readiness +**Request body** (required): `MUCInvitationsEntity` (XML or JSON) - The invitation message to send and whom to send it to. -**Payload:** none +**Responses** -**Return value**: HTTP status 200 (OK). Any HTTP status outside the range 200-399 indicates failure. +| Status | Description | Response body | +|--------|-------------|---------------| +| 200 | Invitation sent. | | +| 403 | Not allowed to invite a user or group to this room. | `ErrorResponse` | +| 404 | The chat room (or its service) can not be found or is not accessible. | `ErrorResponse` | -## Perform 'server' readiness check -Detects if Openfire's core service has been started. ->**GET** /system/readiness/server +## Invite user or group -**Payload:** none +> **POST** /plugins/restapi/v1/chatrooms/{roomName}/invite/{jid} -**Return value**: HTTP status 200 (OK). Any HTTP status outside the range 200-399 indicates failure. +Invites a user or group to join a specific multi-user chat room. -## Perform 'cluster' readiness check -Detects if the cluster functionality has finished starting (or is disabled). ->**GET** /system/readiness/cluster +**Parameters** -**Payload:** none +| Name | Located in | Required | Description | Default value | +|------|------------|----------|-------------|---------------| +| roomName | path | yes | The name of the chat room to which to invite a user or group. Example: `lobby` | | +| jid | path | yes | The entity to invite into the room: the JID of a user or group, or the name of a local user or group. When a group is invited, all of its members are invited. Example: `john@example.org` | | +| servicename | query | no | The name of the chat room's MUC service. Example: `conference` | `conference` | -**Return value**: HTTP status 200 (OK). Any HTTP status outside the range 200-399 indicates failure. +**Request body** (required): `MUCInvitationEntity` (XML or JSON) - The invitation message to send and whom to send it to. -## Perform 'plugins' readiness check -Detects if Openfire has finished starting its plugins. ->**GET** /system/readiness/plugins +**Responses** -**Payload:** none +| Status | Description | Response body | +|--------|-------------|---------------| +| 200 | Invitation sent. | | +| 403 | Not allowed to invite a user to this room. | `ErrorResponse` | +| 404 | The chat room (or its service) can not be found or is not accessible. | `ErrorResponse` | -**Return value**: HTTP status 200 (OK). Any HTTP status outside the range 200-399 indicates failure. +## Get room occupants -## Perform 'connections' readiness check -Detects if Openfire is ready to accept connection requests. ->**GET** /system/readiness/connections +> **GET** /plugins/restapi/v1/chatrooms/{roomName}/occupants -**Payload:** none +Get all occupants of a specific multi-user chat room. -**Return value**: HTTP status 200 (OK). Any HTTP status outside the range 200-399 indicates failure. +**Parameters** -### Possible parameters +| Name | Located in | Required | Description | Default value | +|------|------------|----------|-------------|---------------| +| roomName | path | yes | The name of the chat room for which to return occupants. Example: `lobby` | | +| servicename | query | no | The name of the chat room's MUC service. Example: `conference` | `conference` | -| Parameter | Parameter Type | Description | Default value | -|----------------|----------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------| -| connectionType | @Path | Optional. Use to limit the check to one particular connection type. One of: SOCKET_S2S, SOCKET_C2S, BOSH_C2S, WEBADMIN, COMPONENT, CONNECTION_MANAGER | | -| encypted | @Path | Check the encrypted (true) or unencrypted (false) variant of the connection type. Only used in combination with 'connectionType', as without it, all types and both encrypted and unencrypted are checked. | | +**Responses** -# Group related REST Endpoints +| Status | Description | Response body | +|--------|-------------|---------------| +| 200 | The chat room occupants. | `OccupantEntities` (XML or JSON) | +| 404 | The chat room (or its service) can not be found or is not accessible. | `ErrorResponse` (XML or JSON) | -## Retrieve all groups -Endpoint to get all groups ->**GET** /groups +## Get room participants -**Payload:** none +> **GET** /plugins/restapi/v1/chatrooms/{roomName}/participants -**Return value:** Groups - -### Examples +Get all participants of a specific multi-user chat room. ->**Header**: Authorization: Basic YWRtaW46MTIzNDU= -> ->**GET** http://example.org:9090/plugins/restapi/v1/groups +**Parameters** -## Retrieve a group -Endpoint to get information over specific group ->**GET** /groups/{groupName} +| Name | Located in | Required | Description | Default value | +|------|------------|----------|-------------|---------------| +| roomName | path | yes | The name of the chat room for which to return participants. Example: `lobby` | | +| servicename | query | no | The name of the chat room's MUC service. Example: `conference` | `conference` | -**Payload:** none +**Responses** -**Return value:** Group +| Status | Description | Response body | +|--------|-------------|---------------| +| 200 | The chat room participants. | `ParticipantEntities` (XML or JSON) | +| 404 | The chat room (or its service) can not be found or is not accessible. | `ErrorResponse` (XML or JSON) | -### Possible parameters +## Get room affiliations -| Parameter | Parameter Type | Description | Default value | -|-----------|-----------------|-----------------------|---------------| -| groupName | @Path | The name of the group | | +> **GET** /plugins/restapi/v1/chatrooms/{roomName}/{affiliation} -### Examples +Retrieves a list of JIDs for all users that have a particular affiliation with a multi-user chat room. ->**Header:** Authorization: Basic YWRtaW46MTIzNDU= -> ->**GET** http://example.org:9090/plugins/restapi/v1/groups/moderators +**Parameters** -## Create a group -Endpoint to create a new group ->**POST** /groups +| Name | Located in | Required | Description | Default value | +|------|------------|----------|-------------|---------------| +| servicename | query | no | The name of the MUC service that the MUC room is part of. Example: `conference` | `conference` | +| roomName | path | yes | The name of the MUC room for which to return affiliations. Example: `lobby` | | +| affiliation | path | yes | The type of affiliation. One of: 'admins', 'members', 'outcasts', 'owners'. Example: `members` | | -**Payload:** Group +**Responses** -**Return value:** HTTP status 201 (Created) +| Status | Description | Response body | +|--------|-------------|---------------| +| 200 | Affiliated user list retrieved. | unspecified (XML or JSON) | +| 400 | Provided 'affiliations' value is invalid. | `ErrorResponse` (XML or JSON) | +| 404 | The chat room (or its service) can not be found or is not accessible. | `ErrorResponse` (XML or JSON) | -### Examples +## Add room affiliations ->**Header:** Authorization: Basic YWRtaW46MTIzNDU= -> ->**Header:** Content-Type: application/xml -> ->**POST** http://example.org:9090/plugins/restapi/v1/groups +> **POST** /plugins/restapi/v1/chatrooms/{roomName}/{affiliation} -**Payload Example:** -```xml -<?xml version="1.0" encoding="UTF-8" standalone="yes"?> -<group> - <name>GroupName</name> - <description>Some description</description> - <isshared>false</isshared> -</group> -``` +Affiliates multiple users to a particular multi-user chat room (without removing existing affiliated users of that type). Note that a user can only have one type of affiliation with a room. By affiliating a user to a room, any other pre-existing affiliation for that user is removed. -## Delete a group -Endpoint to delete a group ->**DELETE** /groups/{groupName} +**Parameters** -**Payload:** none +| Name | Located in | Required | Description | Default value | +|------|------------|----------|-------------|---------------| +| servicename | query | no | The name of the MUC service that the MUC room is part of. Example: `conference` | `conference` | +| roomName | path | yes | The name of the MUC room to which users are to be affiliated. Example: `lobby` | | +| affiliation | path | yes | The type of affiliation. One of: 'admins', 'members', 'outcasts', 'owners'. Example: `members` | | +| sendInvitations | query | no | Whether to send invitations to newly affiliated users. Example: `true` | `false` | -**Return value:** HTTP status 200 (OK) +**Request body** (required): `AffiliatedEntities` (XML or JSON) - The list of users to affiliate to the room. -### Possible parameters +**Responses** -| Parameter | Parameter Type | Description | Default value | -|-----------|-----------------|-----------------------|---------------| -| groupName | @Path | The name of the group | | +| Status | Description | Response body | +|--------|-------------|---------------| +| 201 | Users have been affiliated to the room. | | +| 400 | Provided values cannot be parsed as JIDs, or provided 'affiliations' value is invalid. | `ErrorResponse` | +| 403 | Not allowed to perform this affiliation change. | `ErrorResponse` | +| 404 | The chat room (or its service) can not be found or is not accessible. | `ErrorResponse` | -### Examples +## Replace room affiliations ->**Header:** Authorization: Basic YWRtaW46MTIzNDU= -> ->**DELETE** http://example.org:9090/plugins/restapi/v1/groups/groupToDelete +> **PUT** /plugins/restapi/v1/chatrooms/{roomName}/{affiliation} -## Update a group -Endpoint to update / overwrite a group ->**PUT** /groups/{groupName} +Replaces the list of users in a multi-user chat room with a specific affiliation with a new list of users. Note that a user can only have one type of affiliation with a room. By affiliating a user to a room, any other pre-existing affiliation for that user is removed. -**Payload:** Group +**Parameters** -**Return value:** HTTP status 200 (OK) +| Name | Located in | Required | Description | Default value | +|------|------------|----------|-------------|---------------| +| servicename | query | no | The name of the MUC service that the MUC room is part of. Example: `conference` | `conference` | +| roomName | path | yes | The name of the MUC room of which affiliations are to be replaced. Example: `lobby` | | +| affiliation | path | yes | The type of affiliation. One of: 'admins', 'members', 'outcasts', 'owners'. Example: `members` | | +| sendInvitations | query | no | Whether to send invitations to newly affiliated users. Example: `true` | `false` | -### Possible parameters +**Request body** (required): `AffiliatedEntities` (XML or JSON) - The new list of users with this particular affiliation. -| Parameter | Parameter Type | Description | Default value | -|-----------|-----------------|-----------------------|---------------| -| groupName | @Path | The name of the group | | +**Responses** -### Examples ->**Header:** Authorization: Basic YWRtaW46MTIzNDU= -> ->**Header:** Content-Type application/xml -> ->**PUT** http://example.org:9090/plugins/restapi/v1/groups/groupNameToUpdate +| Status | Description | Response body | +|--------|-------------|---------------| +| 201 | Affiliations of the room have been replaced. | | +| 400 | Provided values cannot be parsed as JIDs, or provided 'affiliations' value is invalid. | `ErrorResponse` | +| 403 | Not allowed to perform this affiliation change. | `ErrorResponse` | +| 404 | The chat room (or its service) can not be found or is not accessible. | `ErrorResponse` | -**Payload:** -```xml -<?xml version="1.0" encoding="UTF-8" standalone="yes"?> -<group> - <name>groupNameToUpdate</name> - <description>New description</description> - <isshared>false</isshared> -</group> -``` +## Add group room affiliations -# Session related REST Endpoints +> **POST** /plugins/restapi/v1/chatrooms/{roomName}/{affiliation}/group/{groupname} -## Retrieve all user session -Endpoint to get all user sessions ->**GET** /sessions +Affiliate all members of an Openfire user group to a multi-user chat room. Note that a user can only have one type of affiliation with a room. By affiliating a user to a room, any other pre-existing affiliation for that user is removed. -**Payload:** none +**Parameters** -**Return value:** Sessions - -### Examples +| Name | Located in | Required | Description | Default value | +|------|------------|----------|-------------|---------------| +| servicename | query | no | The name of the MUC service that the MUC room is part of. Example: `conference` | `conference` | +| groupname | path | yes | The name of the user group from which all members will be affiliated to the room. Example: `Operators` | | +| affiliation | path | yes | The type of affiliation. One of: 'admins', 'members', 'outcasts', 'owners'. Example: `members` | | +| roomName | path | yes | The name of the MUC room to which affiliations are to be added. Example: `lobby` | | +| sendInvitations | query | no | Whether to send invitations to newly affiliated users. Example: `true` | `false` | ->**Header**: Authorization: Basic YWRtaW46MTIzNDU= -> ->**GET** http://example.org:9090/plugins/restapi/v1/sessions +**Responses** -## Retrieve the user sessions -Endpoint to get sessions from a user ->**GET** /sessions/{username} +| Status | Description | Response body | +|--------|-------------|---------------| +| 201 | Affiliations added to the room. | | +| 400 | Provided 'affiliations' value is invalid. | `ErrorResponse` | +| 403 | Not allowed to perform this affiliation change. | `ErrorResponse` | +| 404 | The chat room (or its service) can not be found or is not accessible. | `ErrorResponse` | -**Payload:** none +## Remove group room affiliations -**Return value:** Sessions +> **DELETE** /plugins/restapi/v1/chatrooms/{roomName}/{affiliation}/group/{groupname} -### Possible parameters +Removes affiliation for all members of an Openfire user group from a multi-user chat room. -| Parameter | Parameter Type | Description | Default value | -|-----------|-----------------|--------------------------|---------------| -| username | @Path | The username of the user | | +**Parameters** -### Examples +| Name | Located in | Required | Description | Default value | +|------|------------|----------|-------------|---------------| +| groupname | path | yes | The name of the user group from which all members will get their room affiliation removed. Example: `Operators` | | +| servicename | query | no | The name of the MUC service that the MUC room is part of. Example: `conference` | `conference` | +| affiliation | path | yes | The type of affiliation. One of: 'admins', 'members', 'outcasts', 'owners'. Example: `members` | | +| roomName | path | yes | The name of the MUC room from which affiliations are to be removed. Example: `lobby` | | ->**Header:** Authorization: Basic YWRtaW46MTIzNDU= -> ->**GET** http://example.org:9090/plugins/restapi/v1/sessions/testuser +**Responses** -## Close all user sessions -Endpoint to close/kick sessions from a user ->**DELETE** /sessions/{username} +| Status | Description | Response body | +|--------|-------------|---------------| +| 200 | Affiliations removed from the room. | | +| 400 | Provided 'affiliations' value is invalid. | `ErrorResponse` | +| 403 | Not allowed to remove this affiliation. | `ErrorResponse` | +| 404 | The chat room (or its service) can not be found or is not accessible. | `ErrorResponse` | +| 409 | Applying this affiliation change would cause a room conflict. | `ErrorResponse` | -**Payload:** none +## Add room affiliation -**Return value:** HTTP status 200 (OK) +> **POST** /plugins/restapi/v1/chatrooms/{roomName}/{affiliation}/{jid} -### Possible parameters +Affiliates a single user to a multi-user chat room. Note that a user can only have one type of affiliation with a room. By affiliating a user to a room, any other pre-existing affiliation for that user is removed. -| Parameter | Parameter Type | Description | Default value | -|-----------|-----------------|--------------------------|---------------| -| username | @Path | The username of the user | | +**Parameters** -### Examples +| Name | Located in | Required | Description | Default value | +|------|------------|----------|-------------|---------------| +| servicename | query | no | The name of the MUC service that the MUC room is part of. Example: `conference` | `conference` | +| jid | path | yes | The entity that is to be affiliated: a (bare) JID, or the name of a local user. Example: `john@example.org` | | +| affiliation | path | yes | The type of affiliation. One of: 'admins', 'members', 'outcasts', 'owners'. Example: `members` | | +| roomName | path | yes | The name of the MUC room to which an affiliation is to be added. Example: `lobby` | | +| sendInvitations | query | no | Whether to send invitations to newly affiliated users. Example: `true` | `false` | ->**Header:** Authorization: Basic YWRtaW46MTIzNDU= -> ->**DELETE** http://example.org:9090/plugins/restapi/v1/sessions/testuser +**Responses** -# Message related REST Endpoints +| Status | Description | Response body | +|--------|-------------|---------------| +| 201 | User has been affiliated to the room. | | +| 400 | Provided 'affiliations' value is invalid. | `ErrorResponse` | +| 403 | Not allowed to perform this affiliation change. | `ErrorResponse` | +| 404 | The chat room (or its service) can not be found or is not accessible. | `ErrorResponse` | -## Send a broadcast message -Endpoint to send a broadcast/server message to all online users ->**POST** /messages/users +## Remove room affiliation -**Payload:** Message +> **DELETE** /plugins/restapi/v1/chatrooms/{roomName}/{affiliation}/{jid} -**Return value:** HTTP status 201 (Created) - -### Examples +Removes an affiliation of a user to a multi-user chat room. ->**Header**: Authorization: Basic YWRtaW46MTIzNDU= -> ->**POST** http://example.org:9090/plugins/restapi/v1/messages/users +**Parameters** -**Payload:** -```xml -<?xml version="1.0" encoding="UTF-8" standalone="yes"?> -<message> - <body>Your message</body> -</message> -``` -# Security Audit related REST Endpoints +| Name | Located in | Required | Description | Default value | +|------|------------|----------|-------------|---------------| +| jid | path | yes | The entity for which the room affiliation is to be removed: a (bare) JID, or the name of a local user. Example: `john@example.org` | | +| servicename | query | no | The name of the MUC service that the MUC room is part of. Example: `conference` | `conference` | +| affiliation | path | yes | The type of affiliation. One of: 'admins', 'members', 'outcasts', 'owners'. Example: `members` | | +| roomName | path | yes | The name of the MUC room from which an affiliation is to be removed. Example: `lobby` | | -## Retrieve the Security audit logs -Endpoint to get security audit logs ->**GET** /logs/security +**Responses** -**Payload:** none +| Status | Description | Response body | +|--------|-------------|---------------| +| 200 | Affiliation removed from the room. | | +| 400 | Provided 'affiliations' value is invalid. | `ErrorResponse` | +| 403 | Not allowed to remove this affiliation. | `ErrorResponse` | +| 404 | The chat room (or its service) can not be found or is not accessible. | `ErrorResponse` | +| 409 | Applying this affiliation change would cause a room conflict. | `ErrorResponse` | -**Return value:** Security Audit Logs +# Client Sessions -### Possible parameters +Managing live client sessions. -| Parameter | Parameter Type | Description | Default value | -|-----------|----------------|----------------------------------------------------|---------------| -| username | @QueryParam | Username of user to look up | | -| startTime | @QueryParam | Oldest timestamp of range of logs to retrieve | | -| endTime | @QueryParam | Most recent timestamp of range of logs to retrieve | 0 (until now) | -| offset | @QueryParam | Number of logs to skip | | -| limit | @QueryParam | Number of logs to retrieve | | +## Get all sessions -### Examples +> **GET** /plugins/restapi/v1/sessions ->**Header**: Authorization: Basic YWRtaW46MTIzNDU= -> ->**GET** http://example.org:9090/plugins/restapi/v1/logs/security +Retrieve all live client sessions. -# Clustering related REST Endpoints +**Responses** -## Retrieve information for all cluster nodes. -Endpoint to get information for all nodes in the cluster. Note that this endpoint can only return data for remote nodes -when the instance of Openfire that processes this query has successfully joined the cluster. +| Status | Description | Response body | +|--------|-------------|---------------| +| 200 | The client sessions currently active in Openfire. | `SessionEntities` (XML or JSON) | ->**GET** http://example.org:9090/plugins/restapi/v1/clustering/nodes +## Get user sessions -**Payload:** none +> **GET** /plugins/restapi/v1/sessions/{username} -**Return value:** ClusterNodes +Retrieve all live client sessions for a particular user. -### Examples ->**Header:** Authorization: Basic YWRtaW46MTIzNDU= -> ->**GET** http://example.org:9090/plugins/restapi/v1/clustering/nodes -> +**Parameters** -## Retrieve information for a specific cluster node. -Endpoint to get information for a specific cluster node. Note that this endpoint can only return data for remote nodes -when the instance of Openfire that processes this query has successfully joined the cluster. +| Name | Located in | Required | Description | Default value | +|------|------------|----------|-------------|---------------| +| username | path | yes | The name of a user for which to return client sessions. Example: `johndoe` | | ->**GET** http://example.org:9090/plugins/restapi/v1/clustering/nodes/{nodeId} +**Responses** -**Payload:** none +| Status | Description | Response body | +|--------|-------------|---------------| +| 200 | The client sessions for one particular user that are currently active in Openfire. | `SessionEntities` (XML or JSON) | -**Return value:** ClusterNode +## Kick user sessions -### Possible parameters +> **DELETE** /plugins/restapi/v1/sessions/{username} -| Parameter | Parameter Type | Description | Default value | -|-----------|-----------------|--------------|---------------| -| nodeId | @Path | Exact NodeID | | +Close/disconnect all live client sessions for a particular user. -### Examples ->**Header:** Authorization: Basic YWRtaW46MTIzNDU= -> ->**GET** http://example.org:9090/plugins/restapi/v1/clustering/nodes/52a89928-66f7-45fd-9bb8-096de07400ac -> +**Parameters** + +| Name | Located in | Required | Description | Default value | +|------|------------|----------|-------------|---------------| +| username | path | yes | The name of a user for which to drop all client sessions. Example: `johndoe` | | + +**Responses** + +| Status | Description | Response body | +|--------|-------------|---------------| +| 200 | The client sessions for one particular user have been closed. | | + +# Message + +Sending (chat) messages to users. + +## Broadcast + +> **POST** /plugins/restapi/v1/messages/users + +Sends a message to all users that are currently online. + +**Request body** (required): `MessageEntity` (XML or JSON) - The message that is to be broadcast. + +**Responses** + +| Status | Description | Response body | +|--------|-------------|---------------| +| 201 | Message is sent. | | +| 400 | The message content is empty or missing. | | + +# Message Archive + +Server-sided storage of chat messages. + +## Unread message count + +> **GET** /plugins/restapi/v1/archive/messages/unread/{jid} + +Gets a count of messages that haven't been delivered to the user yet. + +**Parameters** + +| Name | Located in | Required | Description | Default value | +|------|------------|----------|-------------|---------------| +| jid | path | yes | The (bare) JID of the user for which the unread message count needs to be fetched. Example: `john@example.org` | | + +**Responses** + +| Status | Description | Response body | +|--------|-------------|---------------| +| 200 | A message count. | `MsgArchiveEntity` (XML or JSON) | + +# Security Audit Log + +Inspecting the security audit log. + +## Get log entries + +> **GET** /plugins/restapi/v1/logs/security + +Retrieve entries from the security audit log. + +**Parameters** + +| Name | Located in | Required | Description | Default value | +|------|------------|----------|-------------|---------------| +| username | query | no | The name of a user for which to filter events. Example: `admin` | | +| offset | query | no | Number of log entries to skip. Example: `0` | | +| limit | query | no | Number of log entries to retrieve. Example: `100` | `100` | +| startTime | query | no | Oldest timestamp of range of logs to retrieve. 0 for 'forever'. | | +| endTime | query | no | Most recent timestamp of range of logs to retrieve. 0 for 'now'. | | + +**Responses** + +| Status | Description | Response body | +|--------|-------------|---------------| +| 200 | The requested log entries. | `SecurityAuditLogs` (XML or JSON) | +| 403 | The audit log is not readable (configured to be write-only). | `ErrorResponse` (XML or JSON) | + +# Statistics + +Inspecting Openfire statistics. + +## Get client session counts + +> **GET** /plugins/restapi/v1/system/statistics/sessions + +Retrieve statistics on the number of client sessions. + +**Responses** + +| Status | Description | Response body | +|--------|-------------|---------------| +| 200 | The requested statistics. | `SessionsCount` (XML or JSON) | + +# System + +Managing Openfire system configuration. + +## Perform all liveness checks + +> **GET** /plugins/restapi/v1/system/liveness + +Detects if Openfire has reached a state that it cannot recover from, except for with a restart, based on every liveness check that it has implemented. + +**Responses** + +| Status | Description | Response body | +|--------|-------------|---------------| +| 200 | The system is live. | | +| 503 | At least one liveness check failed: the system is determined to not be alive. | | + +## Perform 'deadlock' liveness check + +> **GET** /plugins/restapi/v1/system/liveness/deadlock + +Detects if Openfire has reached a state that it cannot recover from because of a deadlock. + +**Responses** + +| Status | Description | Response body | +|--------|-------------|---------------| +| 200 | The system is live. | | +| 503 | A deadlock is detected. | | + +## Perform 'properties' liveness check + +> **GET** /plugins/restapi/v1/system/liveness/properties + +Detects if Openfire has reached a state that it cannot recover from because a system property change requires a restart to take effect. + +**Responses** + +| Status | Description | Response body | +|--------|-------------|---------------| +| 200 | The system is live. | | +| 503 | One or more system property changes that require a server restart have been detected. | | + +## Get system properties + +> **GET** /plugins/restapi/v1/system/properties + +Get all Openfire system properties. + +**Responses** + +| Status | Description | Response body | +|--------|-------------|---------------| +| 200 | The system properties. | `SystemProperties` (XML or JSON) | + +## Create system property + +> **POST** /plugins/restapi/v1/system/properties + +Create a new Openfire system property. Will overwrite a pre-existing system property that uses the same name. + +**Request body** (required): `SystemProperty` (XML or JSON) - The system property to create. + +**Responses** + +| Status | Description | Response body | +|--------|-------------|---------------| +| 201 | The system property is created. | | +| 400 | No system property was provided, the system property has no value, or its name is not valid. The name must consist of one or more dot-separated parts, each consisting of ASCII letters, digits, underscores, apostrophes and hyphens. | `ErrorResponse` | +| 403 | Prohibited to create this system property. | `ErrorResponse` | +| 409 | The name of the system property differs only in case from the name of an existing system property. | `ErrorResponse` | + +## Get system property + +> **GET** /plugins/restapi/v1/system/properties/{propertyKey} + +Get a specific Openfire system property. + +**Parameters** + +| Name | Located in | Required | Description | Default value | +|------|------------|----------|-------------|---------------| +| propertyKey | path | yes | The name of the system property to return. Example: `foo.bar.xyz` | | + +**Responses** + +| Status | Description | Response body | +|--------|-------------|---------------| +| 200 | The requested system property. | `SystemProperty` (XML or JSON) | +| 403 | Reading this system property is prohibited. | `ErrorResponse` (XML or JSON) | +| 404 | The system property could not be found. | `ErrorResponse` (XML or JSON) | + +## Update system property + +> **PUT** /plugins/restapi/v1/system/properties/{propertyKey} + +Updates an existing Openfire system property. + +**Parameters** + +| Name | Located in | Required | Description | Default value | +|------|------------|----------|-------------|---------------| +| propertyKey | path | yes | The name of the system property to update. Example: `foo.bar.xyz` | | + +**Request body** (required): `SystemProperty` (XML or JSON) - The new system property definition that replaces an existing definition. + +**Responses** + +| Status | Description | Response body | +|--------|-------------|---------------| +| 200 | The system property is updated. | | +| 400 | No system property was provided, the system property has no value, or it does not match the name in the URL. | `ErrorResponse` | +| 403 | Prohibited to update this system property. | `ErrorResponse` | +| 404 | The system property could not be found. | `ErrorResponse` | +| 409 | The name of the system property differs only in case from the name of another existing system property. | `ErrorResponse` | + +## Remove system property + +> **DELETE** /plugins/restapi/v1/system/properties/{propertyKey} + +Removes an existing Openfire system property, together with all of its child properties (properties of which the name starts with the name of this property, followed by a dot). + +**Parameters** + +| Name | Located in | Required | Description | Default value | +|------|------------|----------|-------------|---------------| +| propertyKey | path | yes | The name of the system property to delete. Example: `foo.bar.xyz` | | + +**Responses** + +| Status | Description | Response body | +|--------|-------------|---------------| +| 200 | The system property and its child properties are deleted. | | +| 400 | The name of the system property is not valid. It must consist of one or more dot-separated parts, each consisting of ASCII letters, digits, underscores, apostrophes and hyphens. | `ErrorResponse` | +| 403 | Prohibited to delete this system property, or one of its child properties. | `ErrorResponse` | +| 404 | The system property could not be found. | `ErrorResponse` | +| 409 | Deleting this system property could also delete unintended properties (other than this property and its child properties). This can happen, for example, when its name contains an underscore, which can match any character. | `ErrorResponse` | + +## Perform all readiness checks + +> **GET** /plugins/restapi/v1/system/readiness + +Detects if Openfire is in a state where it is ready to process traffic, based on every readiness check that it has implemented. + +**Responses** + +| Status | Description | Response body | +|--------|-------------|---------------| +| 200 | The system is ready. | | +| 503 | At least one readiness check failed: the system is determined to not be able to process traffic. | | + +## Perform 'cluster' readiness check + +> **GET** /plugins/restapi/v1/system/readiness/cluster + +Detects if the cluster functionality has finished starting (or is disabled). + +**Responses** + +| Status | Description | Response body | +|--------|-------------|---------------| +| 200 | The system is ready. | | +| 503 | Clustering functionality is enabled, but has not finished starting up yet. | | + +## Perform 'connections' readiness check + +> **GET** /plugins/restapi/v1/system/readiness/connections + +Detects if Openfire is ready to accept connection requests. + +**Parameters** + +| Name | Located in | Required | Description | Default value | +|------|------------|----------|-------------|---------------| +| connectionType | query | no | Optional. Use to limit the check to one particular connection type. One of: SOCKET_S2S, SOCKET_C2S, BOSH_C2S, WEBADMIN, COMPONENT, CONNECTION_MANAGER. Example: `SOCKET_C2S` | | +| encrypted | query | no | Check the encrypted (true) or unencrypted (false) variant of the connection type. Only used in combination with 'connectionType', as without it, all types and both encrypted and unencrypted are checked. | | + +**Responses** + +| Status | Description | Response body | +|--------|-------------|---------------| +| 200 | The system is ready. | | +| 400 | The provided connectionType value is invalid. | | +| 503 | Openfire currently does not accept (all) connections. | | + +## Perform 'plugins' readiness check + +> **GET** /plugins/restapi/v1/system/readiness/plugins + +Detects if Openfire has finished starting its plugins. + +**Responses** + +| Status | Description | Response body | +|--------|-------------|---------------| +| 200 | The system is ready. | | +| 503 | Plugins have not all been started yet. | | + +## Perform 'server started' readiness check + +> **GET** /plugins/restapi/v1/system/readiness/server + +Detects if Openfire's core service has been started. + +**Responses** + +| Status | Description | Response body | +|--------|-------------|---------------| +| 200 | The system is ready. | | +| 503 | The Openfire service has not finished starting up yet. | | + +# Clustering + +Reporting the status of Openfire clustering. + +## Get all cluster nodes + +> **GET** /plugins/restapi/v1/clustering/nodes + +Get a list of all nodes of the cluster. Note that this endpoint can only return data for remote nodes when the instance of Openfire that processes this query has successfully joined the cluster. + +**Responses** + +| Status | Description | Response body | +|--------|-------------|---------------| +| 200 | All cluster nodes. | `ClusterNodeEntities` (XML or JSON) | + +## Get a specific cluster node + +> **GET** /plugins/restapi/v1/clustering/nodes/{nodeId} + +Get a specific node of the cluster. Note that this endpoint can only return data for remote nodes when the instance of Openfire that processes this query has successfully joined the cluster. + +**Parameters** + +| Name | Located in | Required | Description | Default value | +|------|------------|----------|-------------|---------------| +| nodeId | path | yes | The nodeID value for a particular node. Example: `52a89928-66f7-45fd-9bb8-096de07400ac` | | + +**Responses** + +| Status | Description | Response body | +|--------|-------------|---------------| +| 200 | The cluster node. | `ClusterNodeEntity` (XML or JSON) | +| 404 | The provided NodeID does not identify an existing cluster node. | `ErrorResponse` (XML or JSON) | -## Retrieve the Clustering status -Endpoint to get description of clustering status ->**GET** /clustering/status +## Get clustering status -**Payload:** none +> **GET** /plugins/restapi/v1/clustering/status -**Return value:** String describing the clustering status of this Openfire instance +Describes the point-in-time state of Openfire's clustering with other servers. The status is one of: 'SENIOR AND ONLY MEMBER', 'Senior member', 'Junior member', 'Starting up' or 'Disabled'. -### Examples ->**Header**: Authorization: Basic YWRtaW46MTIzNDU= -> ->**GET** http://example.org:9090/plugins/restapi/v1/clustering/status +**Responses** -### Possible Responses +| Status | Description | Response body | +|--------|-------------|---------------| +| 200 | Status returned. | `ClusteringEntity` (XML or JSON) | -* SENIOR AND ONLY MEMBER -* Senior member -* Junior member -* Starting up -* Disabled +<!-- END GENERATED ENDPOINTS --> # Data format Openfire REST API provides XML and JSON as data format. The default data format is XML. diff --git a/src/build/ReadmeEndpointsGenerator.java b/src/build/ReadmeEndpointsGenerator.java new file mode 100644 index 000000000..439ad12c1 --- /dev/null +++ b/src/build/ReadmeEndpointsGenerator.java @@ -0,0 +1,277 @@ +/* + * Copyright (C) 2026 Ignite Realtime Foundation. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import io.swagger.v3.core.util.Json; +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.Operation; +import io.swagger.v3.oas.models.PathItem; +import io.swagger.v3.oas.models.media.Content; +import io.swagger.v3.oas.models.media.MediaType; +import io.swagger.v3.oas.models.media.Schema; +import io.swagger.v3.oas.models.parameters.Parameter; +import io.swagger.v3.oas.models.parameters.RequestBody; +import io.swagger.v3.oas.models.responses.ApiResponse; +import io.swagger.v3.oas.models.tags.Tag; + +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.TreeMap; + +/** + * Generates the endpoint documentation in readme.md from the OpenAPI specification, which in turn is generated from the + * OpenAPI annotations in the source code. This keeps the endpoint documentation in sync with the implementation. + * + * The generated documentation replaces all text between the {@link #BEGIN_MARKER} and {@link #END_MARKER} lines in the + * readme. Text outside of these markers is not modified. + * + * This is a single-file Java program that is executed by the Maven build. It is not part of the plugin. + * + * Usage: {@code java -cp <classpath> ReadmeEndpointsGenerator.java <openapi.json> <readme.md>} + */ +public class ReadmeEndpointsGenerator +{ + static final String BEGIN_MARKER = "<!-- BEGIN GENERATED ENDPOINTS"; + static final String END_MARKER = "<!-- END GENERATED ENDPOINTS -->"; + + /** The prefix under which the REST API is exposed, relative to the root of the Openfire admin console. */ + static final String CONTEXT_ROOT = "/plugins"; + + /** Order in which tags are documented. Tags that are not listed here are documented after these, alphabetically. */ + static final List<String> TAG_ORDER = List.of("Users", "User Group", "Chat service", "Chat room", "Client Sessions", "Message", "Message Archive", "Security Audit Log", "Statistics", "System", "Clustering"); + + /** Responses that apply to (nearly) all endpoints. These are documented once, instead of for each endpoint. */ + static final Map<String, String> GENERIC_RESPONSES = Map.of( + "401", "Web service authentication failed.", + "500", "Unexpected, generic error condition." + ); + + static final List<PathItem.HttpMethod> METHOD_ORDER = List.of(PathItem.HttpMethod.GET, PathItem.HttpMethod.POST, PathItem.HttpMethod.PUT, PathItem.HttpMethod.PATCH, PathItem.HttpMethod.DELETE, PathItem.HttpMethod.HEAD, PathItem.HttpMethod.OPTIONS, PathItem.HttpMethod.TRACE); + + record Endpoint(String path, PathItem.HttpMethod method, Operation operation) {} + + public static void main(String[] args) throws Exception + { + if (args.length != 2) { + System.err.println("Usage: java ReadmeEndpointsGenerator.java <openapi.json> <readme.md>"); + System.exit(1); + } + final Path specFile = Path.of(args[0]); + final Path readmeFile = Path.of(args[1]); + + final OpenAPI openAPI = Json.mapper().readValue(specFile.toFile(), OpenAPI.class); + final String generated = generate(openAPI); + + final String readme = Files.readString(readmeFile, StandardCharsets.UTF_8); + final int begin = readme.indexOf(BEGIN_MARKER); + final int end = readme.indexOf(END_MARKER); + if (begin < 0 || end < begin) { + throw new IllegalStateException("Unable to find the '" + BEGIN_MARKER + "' and '" + END_MARKER + "' markers in " + readmeFile); + } + final int beginLineEnd = readme.indexOf('\n', begin) + 1; + final String updated = readme.substring(0, beginLineEnd) + "\n" + generated.strip() + "\n\n" + readme.substring(end); + + if (updated.equals(readme)) { + System.out.println("Endpoint documentation in " + readmeFile + " is up to date."); + } else { + Files.writeString(readmeFile, updated, StandardCharsets.UTF_8); + System.out.println("Updated the endpoint documentation in " + readmeFile + "."); + } + } + + static String generate(final OpenAPI openAPI) + { + // Group all endpoints by their (first) tag. + final Map<String, List<Endpoint>> endpointsByTag = new TreeMap<>(Comparator.comparingInt((String tag) -> TAG_ORDER.contains(tag) ? TAG_ORDER.indexOf(tag) : TAG_ORDER.size()).thenComparing(Comparator.naturalOrder())); + openAPI.getPaths().forEach((path, pathItem) -> pathItem.readOperationsMap().forEach((method, operation) -> { + final String tag = operation.getTags() == null || operation.getTags().isEmpty() ? "Other" : operation.getTags().get(0); + endpointsByTag.computeIfAbsent(tag, t -> new ArrayList<>()).add(new Endpoint(path, method, operation)); + })); + + final Map<String, String> tagDescriptions = new LinkedHashMap<>(); + if (openAPI.getTags() != null) { + for (final Tag tag : openAPI.getTags()) { + tagDescriptions.put(tag.getName(), tag.getDescription()); + } + } + + final StringBuilder out = new StringBuilder(); + out.append("# REST Endpoints\n\n"); + out.append("The paths of all endpoints below are relative to the root of the Openfire admin console, for example `http://example.org:9090`.\n\n"); + out.append("In addition to the responses that are documented for each endpoint, every endpoint can respond with:\n\n"); + GENERIC_RESPONSES.entrySet().stream().sorted(Map.Entry.comparingByKey()).forEach(e -> out.append("- `").append(e.getKey()).append("`: ").append(e.getValue()).append("\n")); + out.append("\n"); + out.append("Interactive documentation of these endpoints is available in the Openfire admin console, via the link on the REST API settings page (Server > Server Settings > REST API).\n"); + + endpointsByTag.forEach((tag, endpoints) -> { + out.append("\n# ").append(tag).append("\n\n"); + final String tagDescription = tagDescriptions.get(tag); + if (tagDescription != null && !tagDescription.isBlank()) { + out.append(tagDescription.trim()).append("\n"); + } + + // Keep endpoints for the same resource (sharing the first path segment after the API prefix) together, and + // document resources that have the most general (shortest) paths first. + final Map<String, Integer> resourceDepth = new TreeMap<>(); + endpoints.forEach(e -> resourceDepth.merge(resource(e.path()), depth(e.path()), Math::min)); + endpoints.sort(Comparator.comparingInt((Endpoint e) -> resourceDepth.get(resource(e.path()))) + .thenComparing(e -> resource(e.path())) + .thenComparing(Endpoint::path) + .thenComparingInt(e -> METHOD_ORDER.indexOf(e.method()))); + for (final Endpoint endpoint : endpoints) { + appendEndpoint(out, endpoint); + } + }); + return out.toString().replaceAll("\n{3,}", "\n\n"); + } + + static void appendEndpoint(final StringBuilder out, final Endpoint endpoint) + { + final Operation operation = endpoint.operation(); + out.append("\n## ").append(Optional.ofNullable(operation.getSummary()).orElse(endpoint.method() + " " + endpoint.path())).append("\n\n"); + out.append("> **").append(endpoint.method()).append("** ").append(CONTEXT_ROOT).append(endpoint.path()).append("\n\n"); + if (Boolean.TRUE.equals(operation.getDeprecated())) { + out.append("**Deprecated:** this endpoint may be removed in a future version.\n\n"); + } + if (operation.getDescription() != null && !operation.getDescription().isBlank()) { + out.append(operation.getDescription().trim()).append("\n\n"); + } + + final List<Parameter> parameters = Optional.ofNullable(operation.getParameters()).orElse(List.of()); + if (!parameters.isEmpty()) { + out.append("**Parameters**\n\n"); + out.append("| Name | Located in | Required | Description | Default value |\n"); + out.append("|------|------------|----------|-------------|---------------|\n"); + for (final Parameter parameter : parameters) { + String description = Optional.ofNullable(parameter.getDescription()).orElse(""); + if (parameter.getExample() != null) { + description = (description.isBlank() ? "" : description.trim() + " ") + "Example: `" + parameter.getExample() + "`"; + } else if (parameter.getExamples() != null && !parameter.getExamples().isEmpty()) { + final List<String> examples = new ArrayList<>(); + parameter.getExamples().values().forEach(example -> examples.add("`" + example.getValue() + "`" + (example.getDescription() == null || example.getDescription().isBlank() ? "" : " (" + example.getDescription().trim() + ")"))); + description = (description.isBlank() ? "" : description.trim() + " ") + "Examples: " + String.join(", ", examples); + } + final Object defaultValue = parameter.getSchema() != null ? parameter.getSchema().getDefault() : null; + out.append("| ").append(parameter.getName()) + .append(" | ").append(parameter.getIn()) + .append(" | ").append(Boolean.TRUE.equals(parameter.getRequired()) ? "yes" : "no") + .append(" | ").append(cell(description)) + .append(" | ").append(defaultValue == null ? "" : "`" + cell(defaultValue.toString()) + "`") + .append(" |\n"); + } + out.append("\n"); + } + + final RequestBody requestBody = operation.getRequestBody(); + if (requestBody != null) { + out.append("**Request body**"); + out.append(Boolean.TRUE.equals(requestBody.getRequired()) ? " (required)" : " (optional)"); + out.append(": ").append(describeContent(requestBody.getContent())); + if (requestBody.getDescription() != null && !requestBody.getDescription().isBlank()) { + out.append(" - ").append(requestBody.getDescription().trim()); + } + out.append("\n\n"); + } + + if (operation.getResponses() != null) { + final Map<String, ApiResponse> responses = new TreeMap<>(operation.getResponses()); + responses.entrySet().removeIf(e -> Objects.equals(GENERIC_RESPONSES.get(e.getKey()), e.getValue().getDescription())); + if (!responses.isEmpty()) { + out.append("**Responses**\n\n"); + out.append("| Status | Description | Response body |\n"); + out.append("|--------|-------------|---------------|\n"); + responses.forEach((status, response) -> out.append("| ").append(status) + .append(" | ").append(cell(Optional.ofNullable(response.getDescription()).orElse(""))) + .append(" | ").append(response.getContent() == null || response.getContent().isEmpty() ? "" : describeContent(response.getContent())) + .append(" |\n")); + out.append("\n"); + } + } + } + + /** Describes the data type of content, for example "`UserEntity` (XML or JSON)". */ + static String describeContent(final Content content) + { + if (content == null || content.isEmpty()) { + return "none"; + } + final List<String> formats = new ArrayList<>(); + String type = null; + for (final Map.Entry<String, MediaType> entry : content.entrySet()) { + final String format = switch (entry.getKey()) { + case "application/xml" -> "XML"; + case "application/json" -> "JSON"; + case "text/plain" -> "plain text"; + case "*/*" -> null; + default -> entry.getKey(); + }; + if (format != null) { + formats.add(format); + } + if (type == null && entry.getValue().getSchema() != null) { + type = describeSchema(entry.getValue().getSchema()); + } + } + final StringBuilder result = new StringBuilder(type == null ? "unspecified" : type); + if (!formats.isEmpty()) { + result.append(" (").append(String.join(" or ", formats)).append(")"); + } + return result.toString(); + } + + @SuppressWarnings("rawtypes") + static String describeSchema(final Schema schema) + { + if (schema.get$ref() != null) { + return "`" + schema.get$ref().substring(schema.get$ref().lastIndexOf('/') + 1) + "`"; + } + if ("array".equals(schema.getType()) && schema.getItems() != null) { + return "array of " + describeSchema(schema.getItems()); + } + return schema.getType() == null ? "unspecified" : schema.getType(); + } + + /** Returns the path without the API prefix, for example "users/{username}" for "/restapi/v1/users/{username}". */ + static String relativePath(final String path) + { + return path.replaceFirst("^/restapi/v\\d+/", ""); + } + + /** Returns the name of the resource that is addressed by a path, for example "users" for "/restapi/v1/users/{username}". */ + static String resource(final String path) + { + return relativePath(path).split("/")[0]; + } + + /** Returns the number of path segments of a path, excluding the API prefix. */ + static int depth(final String path) + { + return relativePath(path).split("/").length; + } + + /** Makes text safe for use in a markdown table cell. */ + static String cell(final String text) + { + return text.trim().replace("|", "\\|").replaceAll("\\s*\\R\\s*", "<br>"); + } +} diff --git a/src/java/org/jivesoftware/openfire/plugin/rest/entity/ClusteringEntity.java b/src/java/org/jivesoftware/openfire/plugin/rest/entity/ClusteringEntity.java index 778d52019..4af75d38b 100644 --- a/src/java/org/jivesoftware/openfire/plugin/rest/entity/ClusteringEntity.java +++ b/src/java/org/jivesoftware/openfire/plugin/rest/entity/ClusteringEntity.java @@ -15,6 +15,8 @@ */ package org.jivesoftware.openfire.plugin.rest.entity; +import io.swagger.v3.oas.annotations.media.Schema; + import javax.xml.bind.annotation.XmlElement; import javax.xml.bind.annotation.XmlRootElement; @@ -30,6 +32,7 @@ public ClusteringEntity(String status){ } @XmlElement() + @Schema(description = "The clustering status of this Openfire instance.", allowableValues = {"SENIOR AND ONLY MEMBER", "Senior member", "Junior member", "Starting up", "Disabled"}, example = "Senior member") public String getStatus(){ return status; } diff --git a/src/java/org/jivesoftware/openfire/plugin/rest/service/ClusteringService.java b/src/java/org/jivesoftware/openfire/plugin/rest/service/ClusteringService.java index 54a72d63d..833d10aa8 100644 --- a/src/java/org/jivesoftware/openfire/plugin/rest/service/ClusteringService.java +++ b/src/java/org/jivesoftware/openfire/plugin/rest/service/ClusteringService.java @@ -53,7 +53,7 @@ public void init() { @GET @Path("/status") @Operation( summary = "Get clustering status", - description = "Describes the point-in-time state of Openfire's clustering with other servers.", + description = "Describes the point-in-time state of Openfire's clustering with other servers. The status is one of: 'SENIOR AND ONLY MEMBER', 'Senior member', 'Junior member', 'Starting up' or 'Disabled'.", responses = { @ApiResponse(responseCode = "200", description = "Status returned.", content = @Content(schema = @Schema(implementation = ClusteringEntity.class))), @ApiResponse(responseCode = "401", description = "Web service authentication failed."), diff --git a/src/java/org/jivesoftware/openfire/plugin/rest/service/MUCRoomAffiliationsService.java b/src/java/org/jivesoftware/openfire/plugin/rest/service/MUCRoomAffiliationsService.java index 4fff80d7e..13dc81e5a 100644 --- a/src/java/org/jivesoftware/openfire/plugin/rest/service/MUCRoomAffiliationsService.java +++ b/src/java/org/jivesoftware/openfire/plugin/rest/service/MUCRoomAffiliationsService.java @@ -170,7 +170,7 @@ public Response addMUCRoomAffiliations( }) public Response addMUCRoomAffiliation( @Parameter(description = "The name of the MUC service that the MUC room is part of.", example = "conference", required = false) @DefaultValue("conference") @QueryParam("servicename") String serviceName, - @Parameter(description = "The (bare) JID of the entity that is to be affiliated.", example = "john@example.org", required = true) @PathParam("jid") String jid, + @Parameter(description = "The entity that is to be affiliated: a (bare) JID, or the name of a local user.", example = "john@example.org", required = true) @PathParam("jid") String jid, @Parameter(description = "The type of affiliation. One of: 'admins', 'members', 'outcasts', 'owners'.", example = "members", required = true) @PathParam("affiliation") String affiliations, @Parameter(description = "The name of the MUC room to which an affiliation is to be added.", example = "lobby", required = true) @PathParam("roomName") String roomName, @Parameter(description = "Whether to send invitations to newly affiliated users.", example = "true", required = false) @DefaultValue("false") @QueryParam("sendInvitations") boolean sendInvitations) @@ -232,7 +232,7 @@ public Response addMUCRoomAffiliationGroup( @ApiResponse(responseCode = "500", description = "Unexpected, generic error condition.", content = @Content(schema = @Schema(implementation = ErrorResponse.class))) }) public Response deleteMUCRoomAffiliation( - @Parameter(description = "The (bare) JID of the entity for which the room affiliation is to be removed.", example = "john@example.org", required = true) @PathParam("jid") String jid, + @Parameter(description = "The entity for which the room affiliation is to be removed: a (bare) JID, or the name of a local user.", example = "john@example.org", required = true) @PathParam("jid") String jid, @Parameter(description = "The name of the MUC service that the MUC room is part of.", example = "conference", required = false) @DefaultValue("conference") @QueryParam("servicename") String serviceName, @Parameter(description = "The type of affiliation. One of: 'admins', 'members', 'outcasts', 'owners'.", example = "members", required = true) @PathParam("affiliation") String affiliations, @Parameter(description = "The name of the MUC room from which an affiliation is to be removed.", example = "lobby", required = true) @PathParam("roomName") String roomName) diff --git a/src/java/org/jivesoftware/openfire/plugin/rest/service/MUCRoomService.java b/src/java/org/jivesoftware/openfire/plugin/rest/service/MUCRoomService.java index fdef79aab..9892247fe 100644 --- a/src/java/org/jivesoftware/openfire/plugin/rest/service/MUCRoomService.java +++ b/src/java/org/jivesoftware/openfire/plugin/rest/service/MUCRoomService.java @@ -242,7 +242,7 @@ public MUCRoomMessageEntities getMUCRoomHistory( @Consumes({MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON}) public Response inviteUserOrGroupToMUCRoom( @Parameter(description = "The name of the chat room to which to invite a user or group.", example = "lobby", required = true) @PathParam("roomName") String roomName, - @Parameter(description = "The JID of the entity to invite into the room.", example = "john@example.org", required = true) @PathParam("jid") String jid, + @Parameter(description = "The entity to invite into the room: the JID of a user or group, or the name of a local user or group. When a group is invited, all of its members are invited.", example = "john@example.org", required = true) @PathParam("jid") String jid, @Parameter(description = "The name of the chat room's MUC service.", example = "conference", required = false) @DefaultValue("conference") @QueryParam("servicename") String serviceName, @RequestBody(description = "The invitation message to send and whom to send it to.", required = true) MUCInvitationEntity mucInvitationEntity) throws ServiceException @@ -260,7 +260,7 @@ public Response inviteUserOrGroupToMUCRoom( @POST @Path("/{roomName}/invite") @Operation( summary = "Invite a collection of users and/or groups", - description = "Invites a collection of users and/or groups to join a specific multi-user chat room.", + description = "Invites a collection of users and/or groups to join a specific multi-user chat room. Each entity can be identified by the JID of a user or group, or by the name of a local user or group. When a group is invited, all of its members are invited.", responses = { @ApiResponse(responseCode = "200", description = "Invitation sent."), @ApiResponse(responseCode = "401", description = "Web service authentication failed."), From 83038c09888bd977c6066f0ea42eed4223c00f5d Mon Sep 17 00:00:00 2001 From: Guus der Kinderen <guus.der.kinderen@gmail.com> Date: Fri, 25 Sep 2026 21:03:15 +0200 Subject: [PATCH 3/4] Document the data types of the REST API, and generate their documentation and examples in the readme Adds OpenAPI @Schema annotations (descriptions, examples, required fields and allowed values) to all entities that are used in request and response bodies. The readme generator now also generates the 'Data types' section of the readme from these annotations, replacing the hand-written section that documented only some of the data types (and contained several errors). For every endpoint that accepts a request body, the readme now contains example XML and JSON bodies. These are generated from the examples in the annotations, and are then converted into the entity classes and back using the same JSON and XML serialization as the plugin, which guarantees that they use the actual format of the REST API. The OpenAPI specification described the JSON name of some properties incorrectly: without a Jackson annotation, JSON serialization uses the name of the JAXB annotation, which the OpenAPI generator does not. Jackson annotations that use the actual JSON names have been added (without changing the JSON format), and the readme generator now fails the build when the specification uses a property name that is not used in JSON. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- .github/workflows/build.yml | 8 +- pom.xml | 10 +- readme.md | 1548 ++++++++++++++--- src/build/ReadmeEndpointsGenerator.java | 277 --- src/build/ReadmeGenerator.java | 571 ++++++ .../plugin/rest/entity/AdminEntities.java | 4 + .../rest/entity/AffiliatedEntities.java | 3 + .../rest/entity/ClusterNodeEntities.java | 4 + .../plugin/rest/entity/ClusterNodeEntity.java | 6 + .../plugin/rest/entity/ClusteringEntity.java | 2 + .../plugin/rest/entity/GroupEntities.java | 4 + .../plugin/rest/entity/GroupEntity.java | 11 +- .../rest/entity/MUCInvitationEntity.java | 4 +- .../rest/entity/MUCInvitationsEntity.java | 4 +- .../plugin/rest/entity/MUCRoomEntities.java | 4 + .../plugin/rest/entity/MUCRoomEntity.java | 30 + .../rest/entity/MUCRoomMessageEntities.java | 7 + .../rest/entity/MUCRoomMessageEntity.java | 12 + .../rest/entity/MUCServiceEntities.java | 7 + .../plugin/rest/entity/MUCServiceEntity.java | 8 +- .../plugin/rest/entity/MemberEntities.java | 4 + .../plugin/rest/entity/MessageEntity.java | 4 + .../plugin/rest/entity/MsgArchiveEntity.java | 18 +- .../plugin/rest/entity/OccupantEntities.java | 4 + .../plugin/rest/entity/OccupantEntity.java | 7 + .../plugin/rest/entity/OutcastEntities.java | 4 + .../plugin/rest/entity/OwnerEntities.java | 4 + .../rest/entity/ParticipantEntities.java | 4 + .../plugin/rest/entity/ParticipantEntity.java | 6 + .../entity/RoomCreationResultEntities.java | 8 +- .../rest/entity/RoomCreationResultEntity.java | 7 +- .../plugin/rest/entity/RosterEntities.java | 7 + .../plugin/rest/entity/RosterItemEntity.java | 7 + .../plugin/rest/entity/SecurityAuditLog.java | 9 + .../plugin/rest/entity/SecurityAuditLogs.java | 4 + .../plugin/rest/entity/SessionEntities.java | 4 + .../plugin/rest/entity/SessionEntity.java | 16 + .../plugin/rest/entity/SessionsCount.java | 5 + .../plugin/rest/entity/SystemProperties.java | 7 + .../plugin/rest/entity/SystemProperty.java | 5 + .../plugin/rest/entity/UserEntities.java | 4 + .../plugin/rest/entity/UserEntity.java | 8 + .../plugin/rest/entity/UserGroupsEntity.java | 4 + .../plugin/rest/entity/UserProperty.java | 5 + .../plugin/rest/exceptions/ErrorResponse.java | 7 + .../plugin/rest/service/UserVCardService.java | 18 +- 46 files changed, 2149 insertions(+), 555 deletions(-) delete mode 100644 src/build/ReadmeEndpointsGenerator.java create mode 100644 src/build/ReadmeGenerator.java diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 4580fd45c..0ce9bb2f1 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -8,8 +8,9 @@ jobs: secrets: IGNITE_REALTIME_MAVEN_USERNAME: ${{ secrets.IGNITE_REALTIME_MAVEN_USERNAME }} IGNITE_REALTIME_MAVEN_PASSWORD: ${{ secrets.IGNITE_REALTIME_MAVEN_PASSWORD }} + readme-up-to-date: - name: Check that the readme documents the current endpoints + name: Check that the readme documents the current endpoints and data types runs-on: ubuntu-latest steps: - name: Checkout @@ -22,15 +23,16 @@ jobs: distribution: temurin cache: maven - - name: Regenerate the endpoint documentation + - name: Regenerate the documentation run: mvn -B -DskipTests process-classes - name: Fail when the readme was not up to date run: | if ! git diff --exit-code readme.md; then - echo "::error file=readme.md::The endpoint documentation in readme.md does not match the OpenAPI annotations in the source code. Run 'mvn process-classes' (or any later build phase, like 'mvn package') and commit the updated readme.md." + echo "::error file=readme.md::The documentation of endpoints and data types in readme.md does not match the OpenAPI annotations in the source code. Run 'mvn process-classes' (or any later build phase, like 'mvn package') and commit the updated readme.md." exit 1 fi + hurl-e2e-test: name: Hurl E2E Tests runs-on: ubuntu-latest diff --git a/pom.xml b/pom.xml index 6643089b4..8108c4258 100644 --- a/pom.xml +++ b/pom.xml @@ -47,7 +47,7 @@ <version>3.6.0</version> <!-- Override inherited 2.17 to enable JUnit 5 tests --> </plugin> - <!-- Generate the endpoint documentation in readme.md from the OpenAPI annotations, so that it can't get out of sync with the implementation. --> + <!-- Generate the documentation of endpoints and data types in readme.md from the OpenAPI annotations, so that it can't get out of sync with the implementation. --> <plugin> <!-- Generate the OpenAPI specification from the annotations in the compiled classes. --> <groupId>io.swagger.core.v3</groupId> @@ -73,24 +73,26 @@ </configuration> </plugin> <plugin> - <!-- Replace the endpoint documentation in readme.md with documentation generated from the OpenAPI specification. --> + <!-- Replace the documentation of endpoints and data types in readme.md with documentation generated from the OpenAPI specification. --> <groupId>org.codehaus.mojo</groupId> <artifactId>exec-maven-plugin</artifactId> <version>3.6.4</version> <executions> <execution> - <id>generate-readme-endpoints</id> + <id>generate-readme-documentation</id> <phase>process-classes</phase> <goals> <goal>exec</goal> </goals> <configuration> <executable>${java.home}/bin/java</executable> + <!-- 'compile' includes the 'provided' dependencies (like Openfire), which provide the JAXB implementation that the generator uses. --> + <classpathScope>compile</classpathScope> <arguments> <argument>-Dslf4j.internal.verbosity=ERROR</argument> <argument>-classpath</argument> <classpath/> - <argument>${project.basedir}/src/build/ReadmeEndpointsGenerator.java</argument> + <argument>${project.basedir}/src/build/ReadmeGenerator.java</argument> <argument>${project.build.directory}/openapi/openapi.json</argument> <argument>${project.basedir}/readme.md</argument> </arguments> diff --git a/readme.md b/readme.md index 80492053a..0a8f8928b 100644 --- a/readme.md +++ b/readme.md @@ -136,7 +136,7 @@ Be aware of the following: # REST Endpoints -The paths of all endpoints below are relative to the root of the Openfire admin console, for example `http://example.org:9090`. +The paths of all endpoints below are relative to the root of the Openfire admin console, for example `http://example.org:9090`. The data types that are used by these endpoints are described in [Data types](#data-types). In addition to the responses that are documented for each endpoint, every endpoint can respond with: @@ -167,7 +167,7 @@ Retrieve all users defined in Openfire (with optional filtering). | Status | Description | Response body | |--------|-------------|---------------| -| 200 | A list of Openfire users. | `UserEntities` (XML or JSON) | +| 200 | A list of Openfire users. | [UserEntities](#userentities) (XML or JSON) | ## Create user @@ -175,15 +175,49 @@ Retrieve all users defined in Openfire (with optional filtering). Add a new user to Openfire. -**Request body** (required): `UserEntity` (XML or JSON) - The definition of the user to create. +**Request body** (required): [UserEntity](#userentity) (XML or JSON) - The definition of the user to create. + +<details> +<summary>Example request bodies</summary> + +XML (`Content-Type: application/xml`): + +```xml +<user> + <username>john</username> + <name>John Doe</name> + <email>john@example.org</email> + <password>s3cr3t</password> + <properties> + <property key="department" value="Sales"/> + </properties> +</user> +``` + +JSON (`Content-Type: application/json`): + +```json +{ + "username" : "john", + "name" : "John Doe", + "email" : "john@example.org", + "password" : "s3cr3t", + "properties" : [ { + "key" : "department", + "value" : "Sales" + } ] +} +``` + +</details> **Responses** | Status | Description | Response body | |--------|-------------|---------------| | 201 | The user was created. | | -| 400 | No user definition, username or password was provided. | `ErrorResponse` | -| 409 | A user with this username already exists. | `ErrorResponse` | +| 400 | No user definition, username or password was provided. | [ErrorResponse](#errorresponse) | +| 409 | A user with this username already exists. | [ErrorResponse](#errorresponse) | ## Get user @@ -201,8 +235,8 @@ Retrieve a user that is defined in Openfire. | Status | Description | Response body | |--------|-------------|---------------| -| 200 | The Openfire user. | `UserEntity` (XML or JSON) | -| 404 | No user with that username was found. | `ErrorResponse` (XML or JSON) | +| 200 | The Openfire user. | [UserEntity](#userentity) (XML or JSON) | +| 404 | No user with that username was found. | [ErrorResponse](#errorresponse) (XML or JSON) | ## Update user @@ -216,15 +250,49 @@ Update an existing user in Openfire. |------|------------|----------|-------------|---------------| | username | path | yes | The username of the user to update. | | -**Request body** (required): `UserEntity` (XML or JSON) - The updated definition of the user. +**Request body** (required): [UserEntity](#userentity) (XML or JSON) - The updated definition of the user. + +<details> +<summary>Example request bodies</summary> + +XML (`Content-Type: application/xml`): + +```xml +<user> + <username>john</username> + <name>John Doe</name> + <email>john@example.org</email> + <password>s3cr3t</password> + <properties> + <property key="department" value="Sales"/> + </properties> +</user> +``` + +JSON (`Content-Type: application/json`): + +```json +{ + "username" : "john", + "name" : "John Doe", + "email" : "john@example.org", + "password" : "s3cr3t", + "properties" : [ { + "key" : "department", + "value" : "Sales" + } ] +} +``` + +</details> **Responses** | Status | Description | Response body | |--------|-------------|---------------| | 200 | The user was updated. | | -| 404 | No user with that username was found. | `ErrorResponse` | -| 409 | The user is to be renamed, but a user with the new username already exists. | `ErrorResponse` | +| 404 | No user with that username was found. | [ErrorResponse](#errorresponse) | +| 409 | The user is to be renamed, but a user with the new username already exists. | [ErrorResponse](#errorresponse) | ## Delete user @@ -243,7 +311,7 @@ Remove an existing user from Openfire. | Status | Description | Response body | |--------|-------------|---------------| | 200 | The user was removed. | | -| 404 | No user with that username was found. | `ErrorResponse` | +| 404 | No user with that username was found. | [ErrorResponse](#errorresponse) | ## Get user's groups @@ -261,8 +329,8 @@ Retrieve names of all groups that a particular user is in. | Status | Description | Response body | |--------|-------------|---------------| -| 200 | The names of the groups that the user is in. | `UserGroupsEntity` (XML or JSON) | -| 404 | No user with that username was found. | `ErrorResponse` (XML or JSON) | +| 200 | The names of the groups that the user is in. | [UserGroupsEntity](#usergroupsentity) (XML or JSON) | +| 404 | No user with that username was found. | [ErrorResponse](#errorresponse) (XML or JSON) | ## Add user to groups @@ -276,14 +344,35 @@ Add a particular user to a collection of groups. When a group that is provided d |------|------------|----------|-------------|---------------| | username | path | yes | The username of the user that is to be added to groups. | | -**Request body** (required): `UserGroupsEntity` (XML or JSON) - A collection of names for groups that the user is to be added to. +**Request body** (required): [UserGroupsEntity](#usergroupsentity) (XML or JSON) - A collection of names for groups that the user is to be added to. + +<details> +<summary>Example request bodies</summary> + +XML (`Content-Type: application/xml`): + +```xml +<groups> + <groupname>Sales</groupname> +</groups> +``` + +JSON (`Content-Type: application/json`): + +```json +{ + "groupnames" : [ "Sales" ] +} +``` + +</details> **Responses** | Status | Description | Response body | |--------|-------------|---------------| | 201 | The user was added to all groups. | | -| 400 | The username cannot be parsed into a JID. | `ErrorResponse` | +| 400 | The username cannot be parsed into a JID. | [ErrorResponse](#errorresponse) | ## Delete user from groups @@ -297,14 +386,35 @@ Removes a user from a collection of groups. |------|------------|----------|-------------|---------------| | username | path | yes | The username of the user that is to be removed from groups. | | -**Request body** (required): `UserGroupsEntity` (XML or JSON) - A collection of names for groups from which the user is to be removed. +**Request body** (required): [UserGroupsEntity](#usergroupsentity) (XML or JSON) - A collection of names for groups from which the user is to be removed. + +<details> +<summary>Example request bodies</summary> + +XML (`Content-Type: application/xml`): + +```xml +<groups> + <groupname>Sales</groupname> +</groups> +``` + +JSON (`Content-Type: application/json`): + +```json +{ + "groupnames" : [ "Sales" ] +} +``` + +</details> **Responses** | Status | Description | Response body | |--------|-------------|---------------| | 200 | The user was taken out of the groups. | | -| 404 | One or more groups could not be found. | `ErrorResponse` | +| 404 | One or more groups could not be found. | [ErrorResponse](#errorresponse) | ## Add user to group @@ -324,7 +434,7 @@ Add a particular user to a particular group. When the group does not exist, it w | Status | Description | Response body | |--------|-------------|---------------| | 201 | The user was added to the group. | | -| 400 | The username cannot be parsed into a JID. | `ErrorResponse` | +| 400 | The username cannot be parsed into a JID. | [ErrorResponse](#errorresponse) | ## Delete user from group @@ -344,7 +454,7 @@ Removes a user from a group. | Status | Description | Response body | |--------|-------------|---------------| | 200 | The user was taken out of the group. | | -| 404 | The group could not be found. | `ErrorResponse` | +| 404 | The group could not be found. | [ErrorResponse](#errorresponse) | ## Retrieve user roster @@ -362,8 +472,8 @@ Get a list of all roster entries (buddies / contact list) of a particular user. | Status | Description | Response body | |--------|-------------|---------------| -| 200 | All roster entries. | `RosterEntities` (XML or JSON) | -| 404 | No user with this username exists. | `ErrorResponse` (XML or JSON) | +| 200 | All roster entries. | [RosterEntities](#rosterentities) (XML or JSON) | +| 404 | No user with this username exists. | [ErrorResponse](#errorresponse) (XML or JSON) | ## Create roster entry @@ -377,16 +487,45 @@ Add a roster entry to the roster (buddies / contact list) of a particular user. |------|------------|----------|-------------|---------------| | username | path | yes | The username of the user for which to add a roster entry. | | -**Request body** (required): `RosterItemEntity` (XML or JSON) - The definition of the roster entry that is to be added. +**Request body** (required): [RosterItemEntity](#rosteritementity) (XML or JSON) - The definition of the roster entry that is to be added. + +<details> +<summary>Example request bodies</summary> + +XML (`Content-Type: application/xml`): + +```xml +<rosterItem> + <jid>jane@example.org</jid> + <nickname>Jane</nickname> + <subscriptionType>3</subscriptionType> + <groups> + <group>Friends</group> + </groups> +</rosterItem> +``` + +JSON (`Content-Type: application/json`): + +```json +{ + "jid" : "jane@example.org", + "nickname" : "Jane", + "subscriptionType" : 3, + "groups" : [ "Friends" ] +} +``` + +</details> **Responses** | Status | Description | Response body | |--------|-------------|---------------| | 201 | The entry was added to the roster. | | -| 400 | A roster entry cannot be added to a 'shared group' (try removing group names from the roster entry and try again). | `ErrorResponse` | -| 404 | No user with this username exists. | `ErrorResponse` | -| 409 | A roster entry already exists for the provided contact JID. | `ErrorResponse` | +| 400 | A roster entry cannot be added to a 'shared group' (try removing group names from the roster entry and try again). | [ErrorResponse](#errorresponse) | +| 404 | No user with this username exists. | [ErrorResponse](#errorresponse) | +| 409 | A roster entry already exists for the provided contact JID. | [ErrorResponse](#errorresponse) | ## Update roster entry @@ -401,16 +540,45 @@ Changes a roster entry on the roster (buddies / contact list) of a particular us | username | path | yes | The username of the user for which to update a roster entry. | | | rosterJid | path | yes | The JID of the entry/contact to update. | | -**Request body** (required): `RosterItemEntity` (XML or JSON) - The updated definition of the roster entry. +**Request body** (required): [RosterItemEntity](#rosteritementity) (XML or JSON) - The updated definition of the roster entry. + +<details> +<summary>Example request bodies</summary> + +XML (`Content-Type: application/xml`): + +```xml +<rosterItem> + <jid>jane@example.org</jid> + <nickname>Jane</nickname> + <subscriptionType>3</subscriptionType> + <groups> + <group>Friends</group> + </groups> +</rosterItem> +``` + +JSON (`Content-Type: application/json`): + +```json +{ + "jid" : "jane@example.org", + "nickname" : "Jane", + "subscriptionType" : 3, + "groups" : [ "Friends" ] +} +``` + +</details> **Responses** | Status | Description | Response body | |--------|-------------|---------------| | 200 | The roster entry was updated. | | -| 400 | A roster entry cannot be added with a 'shared group'. | `ErrorResponse` | -| 404 | No user with this username exists. | `ErrorResponse` | -| 409 | A roster entry already exists for the provided contact JID. | `ErrorResponse` | +| 400 | A roster entry cannot be added with a 'shared group'. | [ErrorResponse](#errorresponse) | +| 404 | No user with this username exists. | [ErrorResponse](#errorresponse) | +| 409 | A roster entry already exists for the provided contact JID. | [ErrorResponse](#errorresponse) | ## Remove roster entry @@ -430,8 +598,8 @@ Removes one of the roster entries (contacts) of a particular user. | Status | Description | Response body | |--------|-------------|---------------| | 200 | The entry was removed from the roster. | | -| 400 | A roster entry cannot be removed from a 'shared group'. | `ErrorResponse` | -| 404 | No user with this username exists, or its roster did not contain this entry. | `ErrorResponse` | +| 400 | A roster entry cannot be removed from a 'shared group'. | [ErrorResponse](#errorresponse) | +| 404 | No user with this username exists, or its roster did not contain this entry. | [ErrorResponse](#errorresponse) | ## Get user's vCard @@ -464,15 +632,39 @@ Creates or changes a vCard of a particular user. |------|------------|----------|-------------|---------------| | username | path | yes | The username of the user for which to update the vCard. | | -**Request body** (required): string (XML) - The updated definition of the vCard. +**Request body** (required): string (XML) - The updated definition of the vCard, in the vcard-temp format of XEP-0054. + +<details> +<summary>Example request body</summary> + +`Content-Type: application/xml`: + +```xml +<vCard xmlns="vcard-temp"> + <FN>Janice Francis Doe</FN> + <N> + <FAMILY>Doe</FAMILY> + <GIVEN>Janice</GIVEN> + <MIDDLE>Francis</MIDDLE> + </N> + <NICKNAME>Jane</NICKNAME> + <EMAIL> + <INTERNET/> + <PREF/> + <USERID>j.doe@example.org</USERID> + </EMAIL> +</vCard> +``` + +</details> **Responses** | Status | Description | Response body | |--------|-------------|---------------| | 200 | The vCard was updated/created. | | -| 400 | Provided data could not be parsed. | `ErrorResponse` | -| 409 | Cannot change vCard, as Openfire is configured to have read-only vCards. | `ErrorResponse` | +| 400 | Provided data could not be parsed. | [ErrorResponse](#errorresponse) | +| 409 | Cannot change vCard, as Openfire is configured to have read-only vCards. | [ErrorResponse](#errorresponse) | ## Delete vCard @@ -491,7 +683,7 @@ Removes a vCard of a particular user. | Status | Description | Response body | |--------|-------------|---------------| | 200 | The vCard was deleted. | | -| 409 | Cannot delete vCard, as Openfire is configured to have read-only vCards. | `ErrorResponse` | +| 409 | Cannot delete vCard, as Openfire is configured to have read-only vCards. | [ErrorResponse](#errorresponse) | ## Lock user out @@ -510,7 +702,7 @@ Lockout / ban the user from the chat server. The user will be kicked if the user | Status | Description | Response body | |--------|-------------|---------------| | 201 | The user was locked out. | | -| 404 | No user with this username exists. | `ErrorResponse` | +| 404 | No user with this username exists. | [ErrorResponse](#errorresponse) | ## Unlock user @@ -529,7 +721,7 @@ Removes a previously applied lockout / ban of a user. | Status | Description | Response body | |--------|-------------|---------------| | 200 | User is unlocked. | | -| 404 | No user with this username exists. | `ErrorResponse` | +| 404 | No user with this username exists. | [ErrorResponse](#errorresponse) | # User Group @@ -545,7 +737,7 @@ Get a list of all user groups. | Status | Description | Response body | |--------|-------------|---------------| -| 200 | All groups. | `GroupEntities` (XML or JSON) | +| 200 | All groups. | [GroupEntities](#groupentities) (XML or JSON) | ## Create group @@ -553,15 +745,48 @@ Get a list of all user groups. Create a new user group. -**Request body** (required): `GroupEntity` (XML or JSON) - The group that needs to be created. +**Request body** (required): [GroupEntity](#groupentity) (XML or JSON) - The group that needs to be created. + +<details> +<summary>Example request bodies</summary> + +XML (`Content-Type: application/xml`): + +```xml +<group> + <name>UserGroup1</name> + <description>My group of users</description> + <admins> + <admin>jane.smith</admin> + </admins> + <members> + <member>john.jones</member> + </members> + <shared>false</shared> +</group> +``` + +JSON (`Content-Type: application/json`): + +```json +{ + "name" : "UserGroup1", + "description" : "My group of users", + "admins" : [ "jane.smith" ], + "members" : [ "john.jones" ], + "shared" : false +} +``` + +</details> **Responses** | Status | Description | Response body | |--------|-------------|---------------| | 201 | Group created. | | -| 400 | Group or group name missing, or invalid syntax for a property. | `ErrorResponse` | -| 409 | Group already exists. | `ErrorResponse` | +| 400 | Group or group name missing, or invalid syntax for a property. | [ErrorResponse](#errorresponse) | +| 409 | Group already exists. | [ErrorResponse](#errorresponse) | ## Get group @@ -579,8 +804,8 @@ Get one specific user group by name. | Status | Description | Response body | |--------|-------------|---------------| -| 200 | The group. | `GroupEntity` (XML or JSON) | -| 404 | Group with this name not found. | `ErrorResponse` (XML or JSON) | +| 200 | The group. | [GroupEntity](#groupentity) (XML or JSON) | +| 404 | Group with this name not found. | [ErrorResponse](#errorresponse) (XML or JSON) | ## Update group @@ -594,15 +819,48 @@ Updates / overwrites an existing user group. Note that the name of the group can |------|------------|----------|-------------|---------------| | groupName | path | yes | The name of the group that needs to be updated. Example: `Colleagues` | | -**Request body** (required): `GroupEntity` (XML or JSON) - The new group definition that needs to overwrite the old definition. +**Request body** (required): [GroupEntity](#groupentity) (XML or JSON) - The new group definition that needs to overwrite the old definition. + +<details> +<summary>Example request bodies</summary> + +XML (`Content-Type: application/xml`): + +```xml +<group> + <name>UserGroup1</name> + <description>My group of users</description> + <admins> + <admin>jane.smith</admin> + </admins> + <members> + <member>john.jones</member> + </members> + <shared>false</shared> +</group> +``` + +JSON (`Content-Type: application/json`): + +```json +{ + "name" : "UserGroup1", + "description" : "My group of users", + "admins" : [ "jane.smith" ], + "members" : [ "john.jones" ], + "shared" : false +} +``` + +</details> **Responses** | Status | Description | Response body | |--------|-------------|---------------| | 200 | Group updated. | | -| 400 | Group or group name missing, or name does not match existing group, or invalid syntax for a property. | `ErrorResponse` | -| 404 | Group with this name not found. | `ErrorResponse` | +| 400 | Group or group name missing, or name does not match existing group, or invalid syntax for a property. | [ErrorResponse](#errorresponse) | +| 404 | Group with this name not found. | [ErrorResponse](#errorresponse) | ## Delete group @@ -621,7 +879,7 @@ Removes an existing user group. | Status | Description | Response body | |--------|-------------|---------------| | 200 | Group deleted. | | -| 404 | Group with this name not found. | `ErrorResponse` | +| 404 | Group with this name not found. | [ErrorResponse](#errorresponse) | # Chat service @@ -637,7 +895,7 @@ Get a list of all multi-user chat services. | Status | Description | Response body | |--------|-------------|---------------| -| 200 | All chat services. | `MUCServiceEntities` (XML or JSON) | +| 200 | All chat services. | [MUCServiceEntities](#mucserviceentities) (XML or JSON) | ## Create chat service @@ -645,15 +903,40 @@ Get a list of all multi-user chat services. Create a new multi-user chat service. -**Request body** (required): `MUCServiceEntity` (XML or JSON) - The MUC service that needs to be created. +**Request body** (required): [MUCServiceEntity](#mucserviceentity) (XML or JSON) - The MUC service that needs to be created. + +<details> +<summary>Example request bodies</summary> + +XML (`Content-Type: application/xml`): + +```xml +<chatService> + <serviceName>conference</serviceName> + <description>A public service</description> + <hidden>false</hidden> +</chatService> +``` + +JSON (`Content-Type: application/json`): + +```json +{ + "serviceName" : "conference", + "description" : "A public service", + "hidden" : false +} +``` + +</details> **Responses** | Status | Description | Response body | |--------|-------------|---------------| | 201 | Service created. | | -| 403 | Service creation is not permitted. | `ErrorResponse` | -| 409 | Service already exists, or another conflict occurred while creating the service. | `ErrorResponse` | +| 403 | Service creation is not permitted. | [ErrorResponse](#errorresponse) | +| 409 | Service already exists, or another conflict occurred while creating the service. | [ErrorResponse](#errorresponse) | # Chat room @@ -678,8 +961,8 @@ Get a list of all multi-user chat rooms of a particular chat room service. | Status | Description | Response body | |--------|-------------|---------------| -| 200 | All chat rooms. | `MUCRoomEntities` (XML or JSON) | -| 404 | MUC service does not exist or is not accessible. | `ErrorResponse` (XML or JSON) | +| 200 | All chat rooms. | [MUCRoomEntities](#mucroomentities) (XML or JSON) | +| 404 | MUC service does not exist or is not accessible. | [ErrorResponse](#errorresponse) (XML or JSON) | ## Create chat room @@ -694,16 +977,111 @@ Create a new multi-user chat room. | servicename | query | no | The name of the MUC service in which to create a chat room. Example: `conference` | `conference` | | sendInvitations | query | no | Whether to send invitations to newly affiliated users. Example: `true` | `false` | -**Request body** (required): `MUCRoomEntity` (XML or JSON) - The MUC room that needs to be created. +**Request body** (required): [MUCRoomEntity](#mucroomentity) (XML or JSON) - The MUC room that needs to be created. + +<details> +<summary>Example request bodies</summary> + +XML (`Content-Type: application/xml`): + +```xml +<chatRoom> + <roomName>global</roomName> + <naturalName>Global Chat</naturalName> + <description>A room for everyone</description> + <password>s3cr3t</password> + <subject>Welcome!</subject> + <creationDate>2026-01-31T12:34:56.789Z</creationDate> + <modificationDate>2026-01-31T12:34:56.789Z</modificationDate> + <maxUsers>30</maxUsers> + <persistent>true</persistent> + <publicRoom>true</publicRoom> + <registrationEnabled>false</registrationEnabled> + <canAnyoneDiscoverJID>false</canAnyoneDiscoverJID> + <canOccupantsChangeSubject>false</canOccupantsChangeSubject> + <canOccupantsInvite>false</canOccupantsInvite> + <canChangeNickname>true</canChangeNickname> + <logEnabled>true</logEnabled> + <loginRestrictedToNickname>false</loginRestrictedToNickname> + <membersOnly>false</membersOnly> + <moderated>false</moderated> + <broadcastPresenceRoles> + <broadcastPresenceRole>moderator</broadcastPresenceRole> + </broadcastPresenceRoles> + <owners> + <owner>admin@example.org</owner> + </owners> + <admins> + <admin>jane@example.org</admin> + </admins> + <members> + <member>john@example.org</member> + </members> + <outcasts> + <outcast>spammer@example.org</outcast> + </outcasts> + <ownerGroups> + <ownerGroup>Management</ownerGroup> + </ownerGroups> + <adminGroups> + <adminGroup>Moderators</adminGroup> + </adminGroups> + <memberGroups> + <memberGroup>Sales</memberGroup> + </memberGroups> + <outcastGroups> + <outcastGroup>Banned</outcastGroup> + </outcastGroups> + <allowPM>anyone</allowPM> +</chatRoom> +``` + +JSON (`Content-Type: application/json`): + +```json +{ + "roomName" : "global", + "naturalName" : "Global Chat", + "description" : "A room for everyone", + "password" : "s3cr3t", + "subject" : "Welcome!", + "creationDate" : 1769862896789, + "modificationDate" : 1769862896789, + "maxUsers" : 30, + "persistent" : true, + "publicRoom" : true, + "registrationEnabled" : false, + "canAnyoneDiscoverJID" : false, + "canOccupantsChangeSubject" : false, + "canOccupantsInvite" : false, + "canChangeNickname" : true, + "logEnabled" : true, + "loginRestrictedToNickname" : false, + "membersOnly" : false, + "moderated" : false, + "broadcastPresenceRoles" : [ "moderator" ], + "owners" : [ "admin@example.org" ], + "admins" : [ "jane@example.org" ], + "members" : [ "john@example.org" ], + "outcasts" : [ "spammer@example.org" ], + "ownerGroups" : [ "Management" ], + "adminGroups" : [ "Moderators" ], + "memberGroups" : [ "Sales" ], + "outcastGroups" : [ "Banned" ], + "allowPM" : "anyone" +} +``` + +</details> **Responses** | Status | Description | Response body | |--------|-------------|---------------| | 201 | Room created. | | -| 403 | Room creation is not permitted. | `ErrorResponse` | -| 404 | MUC service does not exist or is not accessible. | `ErrorResponse` | -| 409 | Room already exists, or another conflict occurred while creating the room. | `ErrorResponse` | +| 403 | Room creation is not permitted. | [ErrorResponse](#errorresponse) | +| 404 | MUC service does not exist or is not accessible. | [ErrorResponse](#errorresponse) | +| 409 | Room already exists, or another conflict occurred while creating the room. | [ErrorResponse](#errorresponse) | ## Create multiple chat rooms @@ -718,14 +1096,113 @@ Create a number of new multi-user chat rooms. | servicename | query | no | The name of the MUC service in which to create the chat rooms. Example: `conference` | `conference` | | sendInvitations | query | no | Whether to send invitations to newly affiliated users. Example: `true` | `false` | -**Request body** (required): `MUCRoomEntities` (XML or JSON) - The MUC rooms that need to be created. - -**Responses** - -| Status | Description | Response body | -|--------|-------------|---------------| -| 200 | Request has been processed. Results are reported in the response. | `RoomCreationResultEntities` (XML or JSON) | -| 404 | MUC service does not exist or is not accessible. | `ErrorResponse` (XML or JSON) | +**Request body** (required): [MUCRoomEntities](#mucroomentities) (XML or JSON) - The MUC rooms that need to be created. + +<details> +<summary>Example request bodies</summary> + +XML (`Content-Type: application/xml`): + +```xml +<chatRooms> + <chatRoom> + <roomName>global</roomName> + <naturalName>Global Chat</naturalName> + <description>A room for everyone</description> + <password>s3cr3t</password> + <subject>Welcome!</subject> + <creationDate>2026-01-31T12:34:56.789Z</creationDate> + <modificationDate>2026-01-31T12:34:56.789Z</modificationDate> + <maxUsers>30</maxUsers> + <persistent>true</persistent> + <publicRoom>true</publicRoom> + <registrationEnabled>false</registrationEnabled> + <canAnyoneDiscoverJID>false</canAnyoneDiscoverJID> + <canOccupantsChangeSubject>false</canOccupantsChangeSubject> + <canOccupantsInvite>false</canOccupantsInvite> + <canChangeNickname>true</canChangeNickname> + <logEnabled>true</logEnabled> + <loginRestrictedToNickname>false</loginRestrictedToNickname> + <membersOnly>false</membersOnly> + <moderated>false</moderated> + <broadcastPresenceRoles> + <broadcastPresenceRole>moderator</broadcastPresenceRole> + </broadcastPresenceRoles> + <owners> + <owner>admin@example.org</owner> + </owners> + <admins> + <admin>jane@example.org</admin> + </admins> + <members> + <member>john@example.org</member> + </members> + <outcasts> + <outcast>spammer@example.org</outcast> + </outcasts> + <ownerGroups> + <ownerGroup>Management</ownerGroup> + </ownerGroups> + <adminGroups> + <adminGroup>Moderators</adminGroup> + </adminGroups> + <memberGroups> + <memberGroup>Sales</memberGroup> + </memberGroups> + <outcastGroups> + <outcastGroup>Banned</outcastGroup> + </outcastGroups> + <allowPM>anyone</allowPM> + </chatRoom> +</chatRooms> +``` + +JSON (`Content-Type: application/json`): + +```json +{ + "chatRooms" : [ { + "roomName" : "global", + "naturalName" : "Global Chat", + "description" : "A room for everyone", + "password" : "s3cr3t", + "subject" : "Welcome!", + "creationDate" : 1769862896789, + "modificationDate" : 1769862896789, + "maxUsers" : 30, + "persistent" : true, + "publicRoom" : true, + "registrationEnabled" : false, + "canAnyoneDiscoverJID" : false, + "canOccupantsChangeSubject" : false, + "canOccupantsInvite" : false, + "canChangeNickname" : true, + "logEnabled" : true, + "loginRestrictedToNickname" : false, + "membersOnly" : false, + "moderated" : false, + "broadcastPresenceRoles" : [ "moderator" ], + "owners" : [ "admin@example.org" ], + "admins" : [ "jane@example.org" ], + "members" : [ "john@example.org" ], + "outcasts" : [ "spammer@example.org" ], + "ownerGroups" : [ "Management" ], + "adminGroups" : [ "Moderators" ], + "memberGroups" : [ "Sales" ], + "outcastGroups" : [ "Banned" ], + "allowPM" : "anyone" + } ] +} +``` + +</details> + +**Responses** + +| Status | Description | Response body | +|--------|-------------|---------------| +| 200 | Request has been processed. Results are reported in the response. | [RoomCreationResultEntities](#roomcreationresultentities) (XML or JSON) | +| 404 | MUC service does not exist or is not accessible. | [ErrorResponse](#errorresponse) (XML or JSON) | ## Get chat room @@ -745,8 +1222,8 @@ Get information of a specific multi-user chat room. | Status | Description | Response body | |--------|-------------|---------------| -| 200 | The chat room. | `MUCRoomEntity` (XML or JSON) | -| 404 | The chat room (or its service) can not be found or is not accessible. | `ErrorResponse` (XML or JSON) | +| 200 | The chat room. | [MUCRoomEntity](#mucroomentity) (XML or JSON) | +| 404 | The chat room (or its service) can not be found or is not accessible. | [ErrorResponse](#errorresponse) (XML or JSON) | ## Update chat room @@ -762,16 +1239,111 @@ Updates an existing multi-user chat room. | servicename | query | no | The name of the MUC service in which to update a chat room. Example: `conference` | `conference` | | sendInvitations | query | no | Whether to send invitations to newly affiliated users. Example: `true` | `false` | -**Request body** (required): `MUCRoomEntity` (XML or JSON) - The new MUC room definition that needs to overwrite the old definition. +**Request body** (required): [MUCRoomEntity](#mucroomentity) (XML or JSON) - The new MUC room definition that needs to overwrite the old definition. + +<details> +<summary>Example request bodies</summary> + +XML (`Content-Type: application/xml`): + +```xml +<chatRoom> + <roomName>global</roomName> + <naturalName>Global Chat</naturalName> + <description>A room for everyone</description> + <password>s3cr3t</password> + <subject>Welcome!</subject> + <creationDate>2026-01-31T12:34:56.789Z</creationDate> + <modificationDate>2026-01-31T12:34:56.789Z</modificationDate> + <maxUsers>30</maxUsers> + <persistent>true</persistent> + <publicRoom>true</publicRoom> + <registrationEnabled>false</registrationEnabled> + <canAnyoneDiscoverJID>false</canAnyoneDiscoverJID> + <canOccupantsChangeSubject>false</canOccupantsChangeSubject> + <canOccupantsInvite>false</canOccupantsInvite> + <canChangeNickname>true</canChangeNickname> + <logEnabled>true</logEnabled> + <loginRestrictedToNickname>false</loginRestrictedToNickname> + <membersOnly>false</membersOnly> + <moderated>false</moderated> + <broadcastPresenceRoles> + <broadcastPresenceRole>moderator</broadcastPresenceRole> + </broadcastPresenceRoles> + <owners> + <owner>admin@example.org</owner> + </owners> + <admins> + <admin>jane@example.org</admin> + </admins> + <members> + <member>john@example.org</member> + </members> + <outcasts> + <outcast>spammer@example.org</outcast> + </outcasts> + <ownerGroups> + <ownerGroup>Management</ownerGroup> + </ownerGroups> + <adminGroups> + <adminGroup>Moderators</adminGroup> + </adminGroups> + <memberGroups> + <memberGroup>Sales</memberGroup> + </memberGroups> + <outcastGroups> + <outcastGroup>Banned</outcastGroup> + </outcastGroups> + <allowPM>anyone</allowPM> +</chatRoom> +``` + +JSON (`Content-Type: application/json`): + +```json +{ + "roomName" : "global", + "naturalName" : "Global Chat", + "description" : "A room for everyone", + "password" : "s3cr3t", + "subject" : "Welcome!", + "creationDate" : 1769862896789, + "modificationDate" : 1769862896789, + "maxUsers" : 30, + "persistent" : true, + "publicRoom" : true, + "registrationEnabled" : false, + "canAnyoneDiscoverJID" : false, + "canOccupantsChangeSubject" : false, + "canOccupantsInvite" : false, + "canChangeNickname" : true, + "logEnabled" : true, + "loginRestrictedToNickname" : false, + "membersOnly" : false, + "moderated" : false, + "broadcastPresenceRoles" : [ "moderator" ], + "owners" : [ "admin@example.org" ], + "admins" : [ "jane@example.org" ], + "members" : [ "john@example.org" ], + "outcasts" : [ "spammer@example.org" ], + "ownerGroups" : [ "Management" ], + "adminGroups" : [ "Moderators" ], + "memberGroups" : [ "Sales" ], + "outcastGroups" : [ "Banned" ], + "allowPM" : "anyone" +} +``` + +</details> **Responses** | Status | Description | Response body | |--------|-------------|---------------| | 200 | Room updated. | | -| 403 | Room update/create is not permitted. | `ErrorResponse` | -| 404 | MUC service does not exist or is not accessible. | `ErrorResponse` | -| 409 | This update causes a conflict, possibly with another existing room. | `ErrorResponse` | +| 403 | Room update/create is not permitted. | [ErrorResponse](#errorresponse) | +| 404 | MUC service does not exist or is not accessible. | [ErrorResponse](#errorresponse) | +| 409 | This update causes a conflict, possibly with another existing room. | [ErrorResponse](#errorresponse) | ## Delete chat room @@ -791,7 +1363,7 @@ Removes an existing multi-user chat room. | Status | Description | Response body | |--------|-------------|---------------| | 200 | Room deleted. | | -| 404 | The chat room (or its service) can not be found or is not accessible. | `ErrorResponse` | +| 404 | The chat room (or its service) can not be found or is not accessible. | [ErrorResponse](#errorresponse) | ## Get room history @@ -810,8 +1382,8 @@ Get messages that have been exchanged in a specific multi-user chat room. | Status | Description | Response body | |--------|-------------|---------------| -| 200 | The chat room message history. | `MUCRoomMessageEntities` (XML or JSON) | -| 404 | The chat room (or its service) can not be found or is not accessible. | `ErrorResponse` (XML or JSON) | +| 200 | The chat room message history. | [MUCRoomMessageEntities](#mucroommessageentities) (XML or JSON) | +| 404 | The chat room (or its service) can not be found or is not accessible. | [ErrorResponse](#errorresponse) (XML or JSON) | ## Invite a collection of users and/or groups @@ -826,15 +1398,40 @@ Invites a collection of users and/or groups to join a specific multi-user chat r | roomName | path | yes | The name of the chat room to which to invite users and/or groups. Example: `lobby` | | | servicename | query | no | The name of the chat room's MUC service. Example: `conference` | `conference` | -**Request body** (required): `MUCInvitationsEntity` (XML or JSON) - The invitation message to send and whom to send it to. +**Request body** (required): [MUCInvitationsEntity](#mucinvitationsentity) (XML or JSON) - The invitation message to send and whom to send it to. + +<details> +<summary>Example request bodies</summary> + +XML (`Content-Type: application/xml`): + +```xml +<mucInvitations> + <reason>Come join this cool room please!</reason> + <jidsToInvite> + <jid>john@example.org</jid> + </jidsToInvite> +</mucInvitations> +``` + +JSON (`Content-Type: application/json`): + +```json +{ + "reason" : "Come join this cool room please!", + "jidsToInvite" : [ "john@example.org" ] +} +``` + +</details> **Responses** | Status | Description | Response body | |--------|-------------|---------------| | 200 | Invitation sent. | | -| 403 | Not allowed to invite a user or group to this room. | `ErrorResponse` | -| 404 | The chat room (or its service) can not be found or is not accessible. | `ErrorResponse` | +| 403 | Not allowed to invite a user or group to this room. | [ErrorResponse](#errorresponse) | +| 404 | The chat room (or its service) can not be found or is not accessible. | [ErrorResponse](#errorresponse) | ## Invite user or group @@ -850,15 +1447,36 @@ Invites a user or group to join a specific multi-user chat room. | jid | path | yes | The entity to invite into the room: the JID of a user or group, or the name of a local user or group. When a group is invited, all of its members are invited. Example: `john@example.org` | | | servicename | query | no | The name of the chat room's MUC service. Example: `conference` | `conference` | -**Request body** (required): `MUCInvitationEntity` (XML or JSON) - The invitation message to send and whom to send it to. +**Request body** (required): [MUCInvitationEntity](#mucinvitationentity) (XML or JSON) - The invitation message to send and whom to send it to. + +<details> +<summary>Example request bodies</summary> + +XML (`Content-Type: application/xml`): + +```xml +<mucInvitation> + <reason>Come join this cool room please!</reason> +</mucInvitation> +``` + +JSON (`Content-Type: application/json`): + +```json +{ + "reason" : "Come join this cool room please!" +} +``` + +</details> **Responses** | Status | Description | Response body | |--------|-------------|---------------| | 200 | Invitation sent. | | -| 403 | Not allowed to invite a user to this room. | `ErrorResponse` | -| 404 | The chat room (or its service) can not be found or is not accessible. | `ErrorResponse` | +| 403 | Not allowed to invite a user to this room. | [ErrorResponse](#errorresponse) | +| 404 | The chat room (or its service) can not be found or is not accessible. | [ErrorResponse](#errorresponse) | ## Get room occupants @@ -877,8 +1495,8 @@ Get all occupants of a specific multi-user chat room. | Status | Description | Response body | |--------|-------------|---------------| -| 200 | The chat room occupants. | `OccupantEntities` (XML or JSON) | -| 404 | The chat room (or its service) can not be found or is not accessible. | `ErrorResponse` (XML or JSON) | +| 200 | The chat room occupants. | [OccupantEntities](#occupantentities) (XML or JSON) | +| 404 | The chat room (or its service) can not be found or is not accessible. | [ErrorResponse](#errorresponse) (XML or JSON) | ## Get room participants @@ -897,8 +1515,8 @@ Get all participants of a specific multi-user chat room. | Status | Description | Response body | |--------|-------------|---------------| -| 200 | The chat room participants. | `ParticipantEntities` (XML or JSON) | -| 404 | The chat room (or its service) can not be found or is not accessible. | `ErrorResponse` (XML or JSON) | +| 200 | The chat room participants. | [ParticipantEntities](#participantentities) (XML or JSON) | +| 404 | The chat room (or its service) can not be found or is not accessible. | [ErrorResponse](#errorresponse) (XML or JSON) | ## Get room affiliations @@ -919,8 +1537,8 @@ Retrieves a list of JIDs for all users that have a particular affiliation with a | Status | Description | Response body | |--------|-------------|---------------| | 200 | Affiliated user list retrieved. | unspecified (XML or JSON) | -| 400 | Provided 'affiliations' value is invalid. | `ErrorResponse` (XML or JSON) | -| 404 | The chat room (or its service) can not be found or is not accessible. | `ErrorResponse` (XML or JSON) | +| 400 | Provided 'affiliations' value is invalid. | [ErrorResponse](#errorresponse) (XML or JSON) | +| 404 | The chat room (or its service) can not be found or is not accessible. | [ErrorResponse](#errorresponse) (XML or JSON) | ## Add room affiliations @@ -937,16 +1555,16 @@ Affiliates multiple users to a particular multi-user chat room (without removing | affiliation | path | yes | The type of affiliation. One of: 'admins', 'members', 'outcasts', 'owners'. Example: `members` | | | sendInvitations | query | no | Whether to send invitations to newly affiliated users. Example: `true` | `false` | -**Request body** (required): `AffiliatedEntities` (XML or JSON) - The list of users to affiliate to the room. +**Request body** (required): [AffiliatedEntities](#affiliatedentities) (XML or JSON) - The list of users to affiliate to the room. **Responses** | Status | Description | Response body | |--------|-------------|---------------| | 201 | Users have been affiliated to the room. | | -| 400 | Provided values cannot be parsed as JIDs, or provided 'affiliations' value is invalid. | `ErrorResponse` | -| 403 | Not allowed to perform this affiliation change. | `ErrorResponse` | -| 404 | The chat room (or its service) can not be found or is not accessible. | `ErrorResponse` | +| 400 | Provided values cannot be parsed as JIDs, or provided 'affiliations' value is invalid. | [ErrorResponse](#errorresponse) | +| 403 | Not allowed to perform this affiliation change. | [ErrorResponse](#errorresponse) | +| 404 | The chat room (or its service) can not be found or is not accessible. | [ErrorResponse](#errorresponse) | ## Replace room affiliations @@ -963,16 +1581,16 @@ Replaces the list of users in a multi-user chat room with a specific affiliation | affiliation | path | yes | The type of affiliation. One of: 'admins', 'members', 'outcasts', 'owners'. Example: `members` | | | sendInvitations | query | no | Whether to send invitations to newly affiliated users. Example: `true` | `false` | -**Request body** (required): `AffiliatedEntities` (XML or JSON) - The new list of users with this particular affiliation. +**Request body** (required): [AffiliatedEntities](#affiliatedentities) (XML or JSON) - The new list of users with this particular affiliation. **Responses** | Status | Description | Response body | |--------|-------------|---------------| | 201 | Affiliations of the room have been replaced. | | -| 400 | Provided values cannot be parsed as JIDs, or provided 'affiliations' value is invalid. | `ErrorResponse` | -| 403 | Not allowed to perform this affiliation change. | `ErrorResponse` | -| 404 | The chat room (or its service) can not be found or is not accessible. | `ErrorResponse` | +| 400 | Provided values cannot be parsed as JIDs, or provided 'affiliations' value is invalid. | [ErrorResponse](#errorresponse) | +| 403 | Not allowed to perform this affiliation change. | [ErrorResponse](#errorresponse) | +| 404 | The chat room (or its service) can not be found or is not accessible. | [ErrorResponse](#errorresponse) | ## Add group room affiliations @@ -995,9 +1613,9 @@ Affiliate all members of an Openfire user group to a multi-user chat room. Note | Status | Description | Response body | |--------|-------------|---------------| | 201 | Affiliations added to the room. | | -| 400 | Provided 'affiliations' value is invalid. | `ErrorResponse` | -| 403 | Not allowed to perform this affiliation change. | `ErrorResponse` | -| 404 | The chat room (or its service) can not be found or is not accessible. | `ErrorResponse` | +| 400 | Provided 'affiliations' value is invalid. | [ErrorResponse](#errorresponse) | +| 403 | Not allowed to perform this affiliation change. | [ErrorResponse](#errorresponse) | +| 404 | The chat room (or its service) can not be found or is not accessible. | [ErrorResponse](#errorresponse) | ## Remove group room affiliations @@ -1019,10 +1637,10 @@ Removes affiliation for all members of an Openfire user group from a multi-user | Status | Description | Response body | |--------|-------------|---------------| | 200 | Affiliations removed from the room. | | -| 400 | Provided 'affiliations' value is invalid. | `ErrorResponse` | -| 403 | Not allowed to remove this affiliation. | `ErrorResponse` | -| 404 | The chat room (or its service) can not be found or is not accessible. | `ErrorResponse` | -| 409 | Applying this affiliation change would cause a room conflict. | `ErrorResponse` | +| 400 | Provided 'affiliations' value is invalid. | [ErrorResponse](#errorresponse) | +| 403 | Not allowed to remove this affiliation. | [ErrorResponse](#errorresponse) | +| 404 | The chat room (or its service) can not be found or is not accessible. | [ErrorResponse](#errorresponse) | +| 409 | Applying this affiliation change would cause a room conflict. | [ErrorResponse](#errorresponse) | ## Add room affiliation @@ -1045,9 +1663,9 @@ Affiliates a single user to a multi-user chat room. Note that a user can only ha | Status | Description | Response body | |--------|-------------|---------------| | 201 | User has been affiliated to the room. | | -| 400 | Provided 'affiliations' value is invalid. | `ErrorResponse` | -| 403 | Not allowed to perform this affiliation change. | `ErrorResponse` | -| 404 | The chat room (or its service) can not be found or is not accessible. | `ErrorResponse` | +| 400 | Provided 'affiliations' value is invalid. | [ErrorResponse](#errorresponse) | +| 403 | Not allowed to perform this affiliation change. | [ErrorResponse](#errorresponse) | +| 404 | The chat room (or its service) can not be found or is not accessible. | [ErrorResponse](#errorresponse) | ## Remove room affiliation @@ -1069,10 +1687,10 @@ Removes an affiliation of a user to a multi-user chat room. | Status | Description | Response body | |--------|-------------|---------------| | 200 | Affiliation removed from the room. | | -| 400 | Provided 'affiliations' value is invalid. | `ErrorResponse` | -| 403 | Not allowed to remove this affiliation. | `ErrorResponse` | -| 404 | The chat room (or its service) can not be found or is not accessible. | `ErrorResponse` | -| 409 | Applying this affiliation change would cause a room conflict. | `ErrorResponse` | +| 400 | Provided 'affiliations' value is invalid. | [ErrorResponse](#errorresponse) | +| 403 | Not allowed to remove this affiliation. | [ErrorResponse](#errorresponse) | +| 404 | The chat room (or its service) can not be found or is not accessible. | [ErrorResponse](#errorresponse) | +| 409 | Applying this affiliation change would cause a room conflict. | [ErrorResponse](#errorresponse) | # Client Sessions @@ -1088,7 +1706,7 @@ Retrieve all live client sessions. | Status | Description | Response body | |--------|-------------|---------------| -| 200 | The client sessions currently active in Openfire. | `SessionEntities` (XML or JSON) | +| 200 | The client sessions currently active in Openfire. | [SessionEntities](#sessionentities) (XML or JSON) | ## Get user sessions @@ -1106,7 +1724,7 @@ Retrieve all live client sessions for a particular user. | Status | Description | Response body | |--------|-------------|---------------| -| 200 | The client sessions for one particular user that are currently active in Openfire. | `SessionEntities` (XML or JSON) | +| 200 | The client sessions for one particular user that are currently active in Openfire. | [SessionEntities](#sessionentities) (XML or JSON) | ## Kick user sessions @@ -1136,7 +1754,28 @@ Sending (chat) messages to users. Sends a message to all users that are currently online. -**Request body** (required): `MessageEntity` (XML or JSON) - The message that is to be broadcast. +**Request body** (required): [MessageEntity](#messageentity) (XML or JSON) - The message that is to be broadcast. + +<details> +<summary>Example request bodies</summary> + +XML (`Content-Type: application/xml`): + +```xml +<message> + <body>The server will be restarted in 5 minutes.</body> +</message> +``` + +JSON (`Content-Type: application/json`): + +```json +{ + "body" : "The server will be restarted in 5 minutes." +} +``` + +</details> **Responses** @@ -1165,7 +1804,7 @@ Gets a count of messages that haven't been delivered to the user yet. | Status | Description | Response body | |--------|-------------|---------------| -| 200 | A message count. | `MsgArchiveEntity` (XML or JSON) | +| 200 | A message count. | [MsgArchiveEntity](#msgarchiveentity) (XML or JSON) | # Security Audit Log @@ -1191,8 +1830,8 @@ Retrieve entries from the security audit log. | Status | Description | Response body | |--------|-------------|---------------| -| 200 | The requested log entries. | `SecurityAuditLogs` (XML or JSON) | -| 403 | The audit log is not readable (configured to be write-only). | `ErrorResponse` (XML or JSON) | +| 200 | The requested log entries. | [SecurityAuditLogs](#securityauditlogs) (XML or JSON) | +| 403 | The audit log is not readable (configured to be write-only). | [ErrorResponse](#errorresponse) (XML or JSON) | # Statistics @@ -1208,7 +1847,7 @@ Retrieve statistics on the number of client sessions. | Status | Description | Response body | |--------|-------------|---------------| -| 200 | The requested statistics. | `SessionsCount` (XML or JSON) | +| 200 | The requested statistics. | [SessionsCount](#sessionscount) (XML or JSON) | # System @@ -1263,7 +1902,7 @@ Get all Openfire system properties. | Status | Description | Response body | |--------|-------------|---------------| -| 200 | The system properties. | `SystemProperties` (XML or JSON) | +| 200 | The system properties. | [SystemProperties](#systemproperties) (XML or JSON) | ## Create system property @@ -1271,16 +1910,36 @@ Get all Openfire system properties. Create a new Openfire system property. Will overwrite a pre-existing system property that uses the same name. -**Request body** (required): `SystemProperty` (XML or JSON) - The system property to create. +**Request body** (required): [SystemProperty](#systemproperty) (XML or JSON) - The system property to create. + +<details> +<summary>Example request bodies</summary> + +XML (`Content-Type: application/xml`): + +```xml +<property key="xmpp.domain" value="example.org"/> +``` + +JSON (`Content-Type: application/json`): + +```json +{ + "key" : "xmpp.domain", + "value" : "example.org" +} +``` + +</details> **Responses** | Status | Description | Response body | |--------|-------------|---------------| | 201 | The system property is created. | | -| 400 | No system property was provided, the system property has no value, or its name is not valid. The name must consist of one or more dot-separated parts, each consisting of ASCII letters, digits, underscores, apostrophes and hyphens. | `ErrorResponse` | -| 403 | Prohibited to create this system property. | `ErrorResponse` | -| 409 | The name of the system property differs only in case from the name of an existing system property. | `ErrorResponse` | +| 400 | No system property was provided, the system property has no value, or its name is not valid. The name must consist of one or more dot-separated parts, each consisting of ASCII letters, digits, underscores, apostrophes and hyphens. | [ErrorResponse](#errorresponse) | +| 403 | Prohibited to create this system property. | [ErrorResponse](#errorresponse) | +| 409 | The name of the system property differs only in case from the name of an existing system property. | [ErrorResponse](#errorresponse) | ## Get system property @@ -1298,9 +1957,9 @@ Get a specific Openfire system property. | Status | Description | Response body | |--------|-------------|---------------| -| 200 | The requested system property. | `SystemProperty` (XML or JSON) | -| 403 | Reading this system property is prohibited. | `ErrorResponse` (XML or JSON) | -| 404 | The system property could not be found. | `ErrorResponse` (XML or JSON) | +| 200 | The requested system property. | [SystemProperty](#systemproperty) (XML or JSON) | +| 403 | Reading this system property is prohibited. | [ErrorResponse](#errorresponse) (XML or JSON) | +| 404 | The system property could not be found. | [ErrorResponse](#errorresponse) (XML or JSON) | ## Update system property @@ -1314,17 +1973,37 @@ Updates an existing Openfire system property. |------|------------|----------|-------------|---------------| | propertyKey | path | yes | The name of the system property to update. Example: `foo.bar.xyz` | | -**Request body** (required): `SystemProperty` (XML or JSON) - The new system property definition that replaces an existing definition. +**Request body** (required): [SystemProperty](#systemproperty) (XML or JSON) - The new system property definition that replaces an existing definition. + +<details> +<summary>Example request bodies</summary> + +XML (`Content-Type: application/xml`): + +```xml +<property key="xmpp.domain" value="example.org"/> +``` + +JSON (`Content-Type: application/json`): + +```json +{ + "key" : "xmpp.domain", + "value" : "example.org" +} +``` + +</details> **Responses** | Status | Description | Response body | |--------|-------------|---------------| | 200 | The system property is updated. | | -| 400 | No system property was provided, the system property has no value, or it does not match the name in the URL. | `ErrorResponse` | -| 403 | Prohibited to update this system property. | `ErrorResponse` | -| 404 | The system property could not be found. | `ErrorResponse` | -| 409 | The name of the system property differs only in case from the name of another existing system property. | `ErrorResponse` | +| 400 | No system property was provided, the system property has no value, or it does not match the name in the URL. | [ErrorResponse](#errorresponse) | +| 403 | Prohibited to update this system property. | [ErrorResponse](#errorresponse) | +| 404 | The system property could not be found. | [ErrorResponse](#errorresponse) | +| 409 | The name of the system property differs only in case from the name of another existing system property. | [ErrorResponse](#errorresponse) | ## Remove system property @@ -1343,10 +2022,10 @@ Removes an existing Openfire system property, together with all of its child pro | Status | Description | Response body | |--------|-------------|---------------| | 200 | The system property and its child properties are deleted. | | -| 400 | The name of the system property is not valid. It must consist of one or more dot-separated parts, each consisting of ASCII letters, digits, underscores, apostrophes and hyphens. | `ErrorResponse` | -| 403 | Prohibited to delete this system property, or one of its child properties. | `ErrorResponse` | -| 404 | The system property could not be found. | `ErrorResponse` | -| 409 | Deleting this system property could also delete unintended properties (other than this property and its child properties). This can happen, for example, when its name contains an underscore, which can match any character. | `ErrorResponse` | +| 400 | The name of the system property is not valid. It must consist of one or more dot-separated parts, each consisting of ASCII letters, digits, underscores, apostrophes and hyphens. | [ErrorResponse](#errorresponse) | +| 403 | Prohibited to delete this system property, or one of its child properties. | [ErrorResponse](#errorresponse) | +| 404 | The system property could not be found. | [ErrorResponse](#errorresponse) | +| 409 | Deleting this system property could also delete unintended properties (other than this property and its child properties). This can happen, for example, when its name contains an underscore, which can match any character. | [ErrorResponse](#errorresponse) | ## Perform all readiness checks @@ -1435,7 +2114,7 @@ Get a list of all nodes of the cluster. Note that this endpoint can only return | Status | Description | Response body | |--------|-------------|---------------| -| 200 | All cluster nodes. | `ClusterNodeEntities` (XML or JSON) | +| 200 | All cluster nodes. | [ClusterNodeEntities](#clusternodeentities) (XML or JSON) | ## Get a specific cluster node @@ -1453,8 +2132,8 @@ Get a specific node of the cluster. Note that this endpoint can only return data | Status | Description | Response body | |--------|-------------|---------------| -| 200 | The cluster node. | `ClusterNodeEntity` (XML or JSON) | -| 404 | The provided NodeID does not identify an existing cluster node. | `ErrorResponse` (XML or JSON) | +| 200 | The cluster node. | [ClusterNodeEntity](#clusternodeentity) (XML or JSON) | +| 404 | The provided NodeID does not identify an existing cluster node. | [ErrorResponse](#errorresponse) (XML or JSON) | ## Get clustering status @@ -1466,7 +2145,7 @@ Describes the point-in-time state of Openfire's clustering with other servers. T | Status | Description | Response body | |--------|-------------|---------------| -| 200 | Status returned. | `ClusteringEntity` (XML or JSON) | +| 200 | Status returned. | [ClusteringEntity](#clusteringentity) (XML or JSON) | <!-- END GENERATED ENDPOINTS --> @@ -1475,121 +2154,486 @@ Openfire REST API provides XML and JSON as data format. The default data format To get a JSON result, please add "**Accept: application/json**" to the request header. If you want to create a resource with JSON data format, please add "**Content-Type: application/json**". +<!-- BEGIN GENERATED DATA TYPES: do not edit this section by hand. It is generated from the OpenAPI annotations in the source code by the Maven build. --> + ## Data types -### ClusterNode - -| Parameter | Optional | Description | -|--------------|----------|-------------------------------------------------------------------------------------| -| hostName | No | The hostname and IP address of the server on which this cluster node is running. | -| nodeID | No | A unique identifier of this cluster node. | -| joinedTime | No | Timestamp when the node joined the cluster. | -| seniorMember | No | Boolean value indicating if the node is currently the senior member of the cluster. | - -### User - -| Parameter | Optional | Description | -|------------|----------|------------------------------------------------------------------------------------------| -| username | No | The username of the user | -| name | Yes | The name of the user | -| email | Yes | The email of the user | -| password | No | The password of the user | -| properties | Yes | List of properties. Property is a key / value object. The key must to be per user unique | - -### RosterItem -| Parameter | Optional | Description | -|------------------|----------|-----------------------------------------------------------------------------------------------------------| -| jid | No | The JID of the roster item | -| nickname | Yes | The nickname for the user when used in this roster | -| subscriptionType | Yes | The subscription type <br> Possible numeric values are: -1 (remove), 0 (none), 1 (to), 2 (from), 3 (both) | -| groups | No | A list of groups to organize roster entries under (e.g. friends, co-workers, etc.) | - -### Chatroom - -| Parameter | Optional | Description | -|---------------------------|----------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| roomName | No | The name/id of the room. Can only contains lowercase and alphanumeric characters. | -| naturalName | No | Also the name of the room, but can contains non alphanumeric characters. It's mainly used for users while discovering rooms hosted by the Multi-User Chat service. | -| description | No | Description text of the room. | -| subject | Yes | Subject of the room. | -| password | Yes | The password that the user must provide to enter the room | -| creationDate | Yes | The date when the room was created. Will be automatically set by creation. Example: 2014-07-10T09:49:12.411+02:00 | -| modificationDate | Yes | The last date when the room's configuration was modified. If the room's configuration was never modified then the initial value will be the same as the creation date. Will be automatically set by update. Example: 2014-07-10T09:49:12.411+02:00 | -| maxUsers | Yes | the maximum number of occupants that can be simultaneously in the room. 0 means unlimited number of occupants. | -| persistent | Yes | Can be "true" or "false". Persistent rooms are saved to the database to make their configurations persistent together with the affiliation of the users. Otherwise the room will be destroyed if the last occupant leave the room. | -| publicRoom | Yes | Can be "true" or "false". True if the room is searchable and visible through service discovery. | -| registrationEnabled | Yes | Can be "true" or "false". True if users are allowed to register with the room. By default, room registration is enabled. | -| canAnyoneDiscoverJID | Yes | Can be "true" or "false". True if every presence packet will include the JID of every occupant. | -| canOccupantsChangeSubject | Yes | Can be "true" or "false". True if participants are allowed to change the room's subject. | -| canOccupantsInvite | Yes | Can be "true" or "false". True if occupants can invite other users to the room. If the room does not require an invitation to enter (i.e. is not members-only) then any occupant can send invitations. On the other hand, if the room is members-only and occupants cannot send invitation then only the room owners and admins are allowed to send invitations. | -| canChangeNickname | Yes | Can be "true" or "false". True if room occupants are allowed to change their nicknames in the room. By default, occupants are allowed to change their nicknames. | -| logEnabled | Yes | Can be "true" or "false". True if the room's conversation is being logged. If logging is activated the room conversation will be saved to the database every couple of minutes. The saving frequency is the same for all the rooms and can be configured by changing the property "xmpp.muc.tasks.log.timeout". | -| loginRestrictedToNickname | Yes | Can be "true" or "false". True if registered users can only join the room using their registered nickname. By default, registered users can join the room using any nickname. | -| membersOnly | Yes | Can be "true" or "false". True if the room requires an invitation to enter. That is if the room is members-only. | -| moderated | Yes | Can be "true" or "false". True if the room in which only those with "voice" may send messages to all occupants. | -| allowPM | Yes | One of "anyone", "participants", "moderators" or "none". Controls who is allowed to send private messages to other occupants in the room. | -| broadcastPresenceRoles | Yes | The list of roles of which presence will be broadcasted to the rest of the occupants. | -| owners | Yes | A collection with the current list of owners. The collection contains the bareJID of the users with owner affiliation. | -| admins | Yes | A collection with the current list of admins. The collection contains the bareJID of the users with admin affiliation. | -| members | Yes | A collection with the current list of room members. The collection contains the bareJID of the users with member affiliation. If the room is not members-only then the list will contain the users that registered with the room and therefore they may have reserved a nickname. | -| outcasts | Yes | A collection with the current list of outcast users. An outcast user is not allowed to join the room again. The collection contains the bareJID of the users with outcast affiliation. | -| ownerGroups | Yes | A collection with the current list of groups with owner affiliation. The collection contains the name only. | -| adminGroups | Yes | A collection with the current list of groups with admin affiliation. The collection contains the name only. | -| memberGroups | Yes | A collection with the current list of groups with member affiliation. The collection contains the name only. | -| outcastGroups | Yes | A collection with the current list of groups with outcast affiliation. The collection contains the name only. | - -### Group - -| Parameter | Optional | Description | -|-------------|----------|------------------------------------------------| -| name | No | The name of the group | -| description | No | The description of the group | -| admins | Yes | A collection with current admins of the group | -| members | Yes | A collection with current members of the group | - -### System Property - -| Parameter | Optional | Description | -|-----------|----------|----------------------------------| -| key | No | The name of the system property | -| value | No | The value of the system property | - -### Session -| Parameter | Optional | Description | -|----------------|----------|-------------------------------------------------------------------------------------------------| -| sessionId | No | Full JID of a user e.g. (testUser@testserver.de/SomeRessource) | -| username | No | The username associated with this session. Can be also "Anonymous". | -| resource | Yes | Resource name | -| node | No | Can be "Local" or "Remote" | -| sessionStatus | No | The current status of this session. Can be "Closed", "Connected", "Authenticated" or "Unknown". | -| presenceStatus | No | The status of this presence packet, a natural-language description of availability status. | -| priority | No | The priority of the session. The valid priority range is -128 through 128. | -| hostAddress | No | The IP address string in textual presentation. | -| hostName | No | The host name for this IP address. | -| creationDate | No | The date the session was created. | -| lastActionDate | No | The time the session last had activity. | -| secure | No | Is "true" if this connection is secure. | - -### Sessions count -| Parameter | Optional | Description | -|-----------------|----------|------------------------------------------------------------------------------------------------------------------------------------------| -| clusterSessions | No | Number of client sessions that are authenticated with the server. This includes anonymous and non-anoymous users from the whole cluster. | -| localSessions | No | Number of client sessions that are authenticated with the server. This includes anonymous and non-anoymous users. | - -### Security Audit Logs -| Parameter | Optional | Description | -|-----------|----------|---------------------------------------------------------------------| -| logId | No | Unique ID of this log | -| username | No | The username of the user who performed this event | -| timestamp | No | The time stamp of when this event occurred | -| summary | No | The summary, or short description of what transpired in the event | -| node | No | The node that triggered the event, usually a hostname or IP address | -| details | No | Detailed information about what occurred in the event | - -### Occupants -| Parameter | Optional | Description | -|-------------|----------|-------------------------| -| jid | No | The JID of the MUC room | -| userAddress | No | The JID of the user | -| role | No | Role of the user | -| affiliation | No | Affiliation of the user | +These are the data types that are used in the request and response bodies of the endpoints. The name of a field is the name that is used in JSON. When XML uses a different name, it is mentioned in the description of the field. + +Date/time values are represented as an ISO-8601 formatted text in XML (for example: `2026-01-31T12:34:56.789Z`), and as the number of milliseconds since the Unix epoch in JSON (for example: `1769862896789`). In JSON request bodies, the ISO-8601 format can also be used. + +### AdminEntities + +A list of entities that have an admin affiliation with a multi-user chat room. + +XML root element: `<admins>` + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| admins | array of string | no | The JIDs (or names of local users) of the entities. In XML, items are represented as `<admin>` elements. Example: `jane@example.org` | + +### AffiliatedEntities + +A list of entities that have a particular affiliation with a multi-user chat room. Depending on the affiliation, this is an AdminEntities, MemberEntities, OutcastEntities or OwnerEntities value. + +### ClusterNodeEntities + +A list of the nodes in an Openfire cluster. + +XML root element: `<clusterNodes>` + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| clusterNodes | array of [ClusterNodeEntity](#clusternodeentity) | no | The nodes of the cluster. In XML, items are represented as `<clusterNode>` elements. | + +### ClusterNodeEntity + +A node in an Openfire cluster. + +XML root element: `<clusterNode>` + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| hostName | string | no | The host name and IP address of the server on which this cluster node is running. Example: `xmpp1.example.org (192.168.0.10)` | +| nodeID | string | no | The unique identifier of this cluster node. Example: `a3f1c2d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d` | +| joinedTime | date-time | no | The moment at which this node joined the cluster. | +| seniorMember | boolean | no | Whether this node currently is the senior member of the cluster. Example: `true` | + +### ClusteringEntity + +The clustering status of an Openfire instance. + +XML root element: `<clustering>` + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| status | string | no | The clustering status of this Openfire instance. Allowed values: `SENIOR AND ONLY MEMBER`, `Senior member`, `Junior member`, `Starting up`, `Disabled`. Example: `Senior member` | + +### ErrorResponse + +A description of an error that occurred while processing a request. + +XML root element: `<error>` + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| resource | string | no | The resource (for example, a username or room name) that the error relates to. Example: `john` | +| message | string | no | A description of the error. Example: `Could not get user` | +| exception | string | no | The type of the error. Example: `UserNotFoundException` | + +### GroupEntities + +A list of Openfire user groups. + +XML root element: `<groups>` + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| groups | array of [GroupEntity](#groupentity) | no | The groups. In XML, items are represented as `<group>` elements. | + +### GroupEntity + +An Openfire user group. + +XML root element: `<group>` + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| name | string | yes | The name of the group. When updating a group, this must be equal to the group name in the path of the request. Example: `UserGroup1` | +| description | string | no | The description of the group. Example: `My group of users` | +| shared | boolean | no | Whether the group is shared: whether it automatically appears in the rosters of its members. Example: `false` | +| admins | array of string | no | The admins of the group. When creating or updating a group, each admin can be identified by a username or a JID. Responses contain (bare) JIDs. In XML, items are represented as `<admin>` elements, wrapped in the `<admins>` element. Example: `jane.smith` | +| members | array of string | no | The members of the group. When creating or updating a group, each member can be identified by a username or a JID. Responses contain (bare) JIDs. In XML, items are represented as `<member>` elements, wrapped in the `<members>` element. Example: `john.jones` | + +### MUCInvitationEntity + +An invitation to join a multi-user chat room. + +XML root element: `<mucInvitation>` + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| reason | string | no | The reason that is included in the invitation message(s). Example: `Come join this cool room please!` | + +### MUCInvitationsEntity + +An invitation for a collection of users and/or groups to join a multi-user chat room. + +XML root element: `<mucInvitations>` + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| reason | string | no | The reason that is included in the invitation message(s). Example: `Come join this cool room please!` | +| jidsToInvite | array of string | no | The users and/or groups to invite into the room, each identified by the JID of a user or group, or by the name of a local user or group. In XML, items are represented as `<jid>` elements, wrapped in the `<jidsToInvite>` element. Example: `john@example.org` | + +### MUCRoomEntities + +A list of multi-user chat rooms. + +XML root element: `<chatRooms>` + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| chatRooms | array of [MUCRoomEntity](#mucroomentity) | no | The chat rooms. In XML, items are represented as `<chatRoom>` elements. | + +### MUCRoomEntity + +A multi-user chat room. When a room is created or updated, boolean values that are not provided are treated as 'false'. + +XML root element: `<chatRoom>` + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| roomName | string | yes | The name of the room, which is used as the local part of the room's JID. It is converted to lowercase. When updating a room, this must be equal to the room name in the path of the request. Example: `global` | +| description | string | no | The description of the room. Example: `A room for everyone` | +| password | string | no | The password that users must provide to enter the room. Example: `s3cr3t` | +| subject | string | no | The subject (topic) of the room. Example: `Welcome!` | +| naturalName | string | no | The human-readable name of the room, as shown to users that discover rooms on the chat service. Example: `Global Chat` | +| maxUsers | integer | no | The maximum number of occupants that can be in the room at the same time. 0 means unlimited. Example: `30` | +| creationDate | date-time | no | The moment at which the room was created. When creating a room without this value, the current time is used. | +| modificationDate | date-time | no | The moment at which the configuration of the room was last modified. When creating or updating a room without this value, the current time is used. | +| persistent | boolean | no | Whether the room is persistent. Persistent rooms are saved to the database, and are not destroyed when the last occupant leaves. Example: `true` | +| publicRoom | boolean | no | Whether the room is public: searchable and visible through service discovery. Example: `true` | +| registrationEnabled | boolean | no | Whether users are allowed to register with the room. Example: `false` | +| canAnyoneDiscoverJID | boolean | no | Whether the real JID of every occupant is visible to every other occupant (a non-anonymous room). Example: `false` | +| canOccupantsChangeSubject | boolean | no | Whether participants are allowed to change the subject of the room. Example: `false` | +| canOccupantsInvite | boolean | no | Whether occupants can invite other users to the room. When the room is not members-only, anyone can send invitations regardless of this value. When the room is members-only and this is 'false', only owners and admins can send invitations. Example: `false` | +| canChangeNickname | boolean | no | Whether occupants are allowed to change their nickname in the room. Example: `true` | +| logEnabled | boolean | no | Whether the conversation in the room is logged (saved to the database). Example: `true` | +| loginRestrictedToNickname | boolean | no | Whether registered users can only join the room using their registered nickname. Example: `false` | +| membersOnly | boolean | no | Whether the room is members-only: users need to be a member (or be invited) to enter. Example: `false` | +| moderated | boolean | no | Whether the room is moderated: only occupants with 'voice' can send messages to all occupants. Example: `false` | +| allowPM | string | no | Defines who is allowed to send private messages to other occupants. Must be one of "anyone", "participants", "moderators" or "none". Example: `anyone` | +| broadcastPresenceRoles | array of string | no | The roles of occupants of which presence is broadcast to the other occupants. Each is one of: 'moderator', 'participant', 'visitor'. In XML, items are represented as `<broadcastPresenceRole>` elements, wrapped in the `<broadcastPresenceRoles>` element. Example: `moderator` | +| owners | array of string | no | The (bare) JIDs of the users that have an owner affiliation with the room. When creating a room without owners, the 'admin' user is made owner. In XML, items are represented as `<owner>` elements, wrapped in the `<owners>` element. Example: `admin@example.org` | +| ownerGroups | array of string | no | The names of the user groups that have an owner affiliation with the room. In XML, items are represented as `<ownerGroup>` elements, wrapped in the `<ownerGroups>` element. Example: `Management` | +| admins | array of string | no | The (bare) JIDs of the users that have an admin affiliation with the room. In XML, items are represented as `<admin>` elements, wrapped in the `<admins>` element. Example: `jane@example.org` | +| adminGroups | array of string | no | The names of the user groups that have an admin affiliation with the room. In XML, items are represented as `<adminGroup>` elements, wrapped in the `<adminGroups>` element. Example: `Moderators` | +| members | array of string | no | The (bare) JIDs of the users that have a member affiliation with the room. In XML, items are represented as `<member>` elements, wrapped in the `<members>` element. Example: `john@example.org` | +| memberGroups | array of string | no | The names of the user groups that have a member affiliation with the room. In XML, items are represented as `<memberGroup>` elements, wrapped in the `<memberGroups>` element. Example: `Sales` | +| outcasts | array of string | no | The (bare) JIDs of the users that have an outcast affiliation with the room: users that are banned from the room. In XML, items are represented as `<outcast>` elements, wrapped in the `<outcasts>` element. Example: `spammer@example.org` | +| outcastGroups | array of string | no | The names of the user groups that have an outcast affiliation with the room. In XML, items are represented as `<outcastGroup>` elements, wrapped in the `<outcastGroups>` element. Example: `Banned` | + +### MUCRoomMessageEntities + +A list of messages from the history of a multi-user chat room. + +XML root element: `<messages>` + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| message | array of [MUCRoomMessageEntity](#mucroommessageentity) | no | The messages. In XML, items are represented as `<message>` elements. | + +### MUCRoomMessageEntity + +A message from the history of a multi-user chat room. + +XML root element: `<message>` + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| to | string | no | The JID of the addressee of the message. Example: `global@conference.example.org` | +| from | string | no | The JID of the sender of the message: the room JID, followed by the nickname of the occupant. Example: `global@conference.example.org/john` | +| type | string | no | The XMPP message type. Example: `groupchat` | +| body | string | no | The text of the message. Example: `Hello, everyone!` | +| delay_stamp | string | no | The moment at which the message was originally sent (XEP-0203 delayed delivery timestamp). Example: `2026-01-31T12:34:56.789Z` | +| delay_from | string | no | The JID of the entity that delayed the delivery of the message (XEP-0203). Example: `global@conference.example.org` | + +### MUCServiceEntities + +A list of multi-user chat services. + +XML root element: `<chatServices>` + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| chatService | array of [MUCServiceEntity](#mucserviceentity) | no | The chat services. In XML, items are represented as `<chatService>` elements. | + +### MUCServiceEntity + +A multi-user chat service. + +XML root element: `<chatService>` + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| serviceName | string | yes | The name of the chat service, which is used as the subdomain of the service. Example: `conference` | +| description | string | no | The description of the chat service. Example: `A public service` | +| hidden | boolean | no | Whether the service is hidden from service discovery. Example: `false` | + +### MemberEntities + +A list of entities that have a member affiliation with a multi-user chat room. + +XML root element: `<members>` + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| members | array of string | no | The JIDs (or names of local users) of the entities. In XML, items are represented as `<member>` elements. Example: `john@example.org` | + +### MessageEntity + +A message. + +XML root element: `<message>` + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| body | string | yes | The text of the message. Example: `The server will be restarted in 5 minutes.` | + +### MsgArchiveEntity + +The number of unread messages of a user. + +XML root element: `<archive>` + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| jid | string | no | The JID of the user. Example: `john@example.org` | +| count | integer | no | The number of unread messages. Example: `3` | + +### OccupantEntities + +A list of occupants of a multi-user chat room. + +XML root element: `<occupants>` + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| occupants | array of [OccupantEntity](#occupantentity) | no | The occupants. In XML, items are represented as `<occupant>` elements. | + +### OccupantEntity + +An occupant of a multi-user chat room. + +XML root element: `<occupant>` + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| jid | string | no | The occupant JID: the room JID, followed by the nickname of the occupant. Example: `global@conference.example.org/john` | +| userAddress | string | no | The real (full) JID of the user. Example: `john@example.org/laptop` | +| role | string | no | The role of the occupant in the room. One of: 'moderator', 'participant', 'visitor', 'none'. Example: `participant` | +| affiliation | string | no | The affiliation of the occupant with the room. One of: 'owner', 'admin', 'member', 'outcast', 'none'. Example: `member` | + +### OutcastEntities + +A list of entities that have an outcast affiliation with a multi-user chat room: entities that are banned from the room. + +XML root element: `<outcasts>` + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| outcasts | array of string | no | The JIDs (or names of local users) of the entities. In XML, items are represented as `<outcast>` elements. Example: `spammer@example.org` | + +### OwnerEntities + +A list of entities that have an owner affiliation with a multi-user chat room. + +XML root element: `<owners>` + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| owners | array of string | no | The JIDs (or names of local users) of the entities. In XML, items are represented as `<owner>` elements. Example: `admin@example.org` | + +### ParticipantEntities + +A list of occupants of a multi-user chat room. + +XML root element: `<participants>` + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| participants | array of [ParticipantEntity](#participantentity) | no | The occupants. In XML, items are represented as `<participant>` elements. | + +### ParticipantEntity + +An occupant of a multi-user chat room. + +XML root element: `<participant>` + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| jid | string | no | The occupant JID: the room JID, followed by the nickname of the occupant. Example: `global@conference.example.org/john` | +| role | string | no | The role of the occupant in the room. One of: 'moderator', 'participant', 'visitor', 'none'. Example: `participant` | +| affiliation | string | no | The affiliation of the occupant with the room. One of: 'owner', 'admin', 'member', 'outcast', 'none'. Example: `member` | + +### RoomCreationResultEntities + +The results of the creation of multiple multi-user chat rooms, grouped by result type. + +XML root element: `<results>` + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| success | array of [RoomCreationResultEntity](#roomcreationresultentity) | no | The results of the rooms that were created successfully. In XML, the items are wrapped in the `<success>` element. | +| failure | array of [RoomCreationResultEntity](#roomcreationresultentity) | no | The results of the rooms that could not be created. In XML, the items are wrapped in the `<failure>` element. | +| other | array of [RoomCreationResultEntity](#roomcreationresultentity) | no | The results of a type other than success or failure. In XML, the items are wrapped in the `<other>` element. | + +### RoomCreationResultEntity + +The result of the creation of one multi-user chat room. + +XML root element: `<result>` + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| roomName | string | no | The name of the room that was to be created. Example: `open_chat` | +| resultType | string | no | The result of creating the room. Allowed values: `Success`, `Failure`. Example: `Failure` | +| message | string | no | A message that describes the result. Example: `Room already existed and therefore not created again` | + +### RosterEntities + +The roster (contact list) of a user. + +XML root element: `<roster>` + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| rosterItem | array of [RosterItemEntity](#rosteritementity) | no | The entries of the roster. In XML, items are represented as `<rosterItem>` elements. | + +### RosterItemEntity + +An entry in the roster (contact list) of a user. + +XML root element: `<rosterItem>` + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| jid | string | yes | The JID of the contact. Example: `jane@example.org` | +| nickname | string | no | The name of the contact, as shown in this roster. Example: `Jane` | +| subscriptionType | integer | no | The presence subscription state of the contact. One of: -1 (remove), 0 (none), 1 (to: the user receives presence updates of the contact), 2 (from: the contact receives presence updates of the user), 3 (both). Example: `3` | +| groups | array of string | no | The roster groups (for example 'Friends' or 'Co-workers') that this contact is organized under. In XML, items are represented as `<group>` elements, wrapped in the `<groups>` element. Example: `Friends` | + +### SecurityAuditLog + +An entry of the security audit log. + +XML root element: `<log>` + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| logId | integer | no | The unique identifier of the log entry. Example: `42` | +| username | string | no | The username of the user that performed the audited action. Example: `admin` | +| timestamp | integer | no | The moment at which the audited action occurred, in seconds since the Unix epoch. Example: `1769862896` | +| summary | string | no | A short description of the audited action. Example: `Created new user john` | +| node | string | no | The node that triggered the audited action, usually a host name or IP address. Example: `xmpp1.example.org` | +| details | string | no | Detailed information about the audited action. Example: `name = John Doe, email = john@example.org` | + +### SecurityAuditLogs + +A list of entries of the security audit log. + +XML root element: `<logs>` + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| logs | array of [SecurityAuditLog](#securityauditlog) | no | The log entries. In XML, items are represented as `<log>` elements. | + +### SessionEntities + +A list of client sessions. + +XML root element: `<sessions>` + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| sessions | array of [SessionEntity](#sessionentity) | no | The sessions. In XML, items are represented as `<session>` elements. | + +### SessionEntity + +A client session. + +XML root element: `<session>` + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| sessionId | string | no | The (full) JID of the session. Example: `john@example.org/laptop` | +| username | string | no | The username of the user of the session, or 'Anonymous' for anonymous sessions. Example: `john` | +| resource | string | no | The resource part of the JID of the session. Example: `laptop` | +| node | string | no | Whether the session is connected to the cluster node that processes the request ('Local'), or to another cluster node ('Remote'). Example: `Local` | +| sessionStatus | string | no | The status of the session. One of: 'Closed', 'Connected', 'Authenticated', 'Unknown'. Example: `Authenticated` | +| presenceStatus | string | no | The availability of the user of the session. One of: 'Online', 'Away', 'Available to Chat', 'Do Not Disturb', 'Extended Away', 'Unknown/Not Recognized'. Example: `Online` | +| presenceMessage | string | no | The (optional) natural-language description of the availability of the user of the session. Example: `In a meeting` | +| priority | integer | no | The presence priority of the session, from -128 to 127. Example: `0` | +| hostAddress | string | no | The IP address of the client. Example: `192.168.0.20` | +| hostName | string | no | The host name of the client. Example: `laptop.example.org` | +| creationDate | date-time | no | The moment at which the session was created. | +| lastActionDate | date-time | no | The moment at which the session last had activity. | +| secure | boolean | no | Whether the connection of the session is encrypted. Example: `true` | + +### SessionsCount + +The number of client sessions. + +XML root element: `<sessions>` + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| localSessions | integer | no | The number of authenticated client sessions (of both anonymous and non-anonymous users) on the cluster node that processes the request. Example: `12` | +| clusterSessions | integer | no | The number of authenticated client sessions (of both anonymous and non-anonymous users) in the entire cluster. Example: `30` | + +### SystemProperties + +A list of Openfire system properties. + +XML root element: `<properties>` + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| property | array of [SystemProperty](#systemproperty) | no | The system properties. In XML, items are represented as `<property>` elements. | + +### SystemProperty + +An Openfire system property. + +XML root element: `<property>` + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| key | string | yes | The name of the system property. In XML, this is the `key` attribute. Example: `xmpp.domain` | +| value | string | yes | The value of the system property. In XML, this is the `value` attribute. Example: `example.org` | + +### UserEntities + +A list of Openfire users. + +XML root element: `<users>` + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| users | array of [UserEntity](#userentity) | no | The users. In XML, items are represented as `<user>` elements. | + +### UserEntity + +An Openfire user. + +XML root element: `<user>` + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| username | string | no | The username of the user. Required when creating a user. When updating a user, providing a different username renames the user. Example: `john` | +| name | string | no | The name of the user. Example: `John Doe` | +| email | string | no | The email address of the user. Example: `john@example.org` | +| password | string | no | The password of the user. Required when creating a user. Never included in responses. Example: `s3cr3t` | +| properties | array of [UserProperty](#userproperty) | no | Custom properties of the user. Property keys are unique per user. When updating a user, all existing properties of the user are replaced by the provided properties: omitting this removes all properties of the user. In XML, the items are wrapped in the `<properties>` element. | + +### UserGroupsEntity + +A list of names of Openfire user groups. + +XML root element: `<groups>` + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| groupnames | array of string | no | The names of the groups. In XML, items are represented as `<groupname>` elements. Example: `Sales` | + +### UserProperty + +A custom property of an Openfire user. + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| key | string | yes | The key (name) of the property. Unique per user. In XML, this is the `key` attribute. Example: `department` | +| value | string | yes | The value of the property. In XML, this is the `value` attribute. Example: `Sales` | + +<!-- END GENERATED DATA TYPES --> diff --git a/src/build/ReadmeEndpointsGenerator.java b/src/build/ReadmeEndpointsGenerator.java deleted file mode 100644 index 439ad12c1..000000000 --- a/src/build/ReadmeEndpointsGenerator.java +++ /dev/null @@ -1,277 +0,0 @@ -/* - * Copyright (C) 2026 Ignite Realtime Foundation. All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import io.swagger.v3.core.util.Json; -import io.swagger.v3.oas.models.OpenAPI; -import io.swagger.v3.oas.models.Operation; -import io.swagger.v3.oas.models.PathItem; -import io.swagger.v3.oas.models.media.Content; -import io.swagger.v3.oas.models.media.MediaType; -import io.swagger.v3.oas.models.media.Schema; -import io.swagger.v3.oas.models.parameters.Parameter; -import io.swagger.v3.oas.models.parameters.RequestBody; -import io.swagger.v3.oas.models.responses.ApiResponse; -import io.swagger.v3.oas.models.tags.Tag; - -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.ArrayList; -import java.util.Comparator; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Optional; -import java.util.TreeMap; - -/** - * Generates the endpoint documentation in readme.md from the OpenAPI specification, which in turn is generated from the - * OpenAPI annotations in the source code. This keeps the endpoint documentation in sync with the implementation. - * - * The generated documentation replaces all text between the {@link #BEGIN_MARKER} and {@link #END_MARKER} lines in the - * readme. Text outside of these markers is not modified. - * - * This is a single-file Java program that is executed by the Maven build. It is not part of the plugin. - * - * Usage: {@code java -cp <classpath> ReadmeEndpointsGenerator.java <openapi.json> <readme.md>} - */ -public class ReadmeEndpointsGenerator -{ - static final String BEGIN_MARKER = "<!-- BEGIN GENERATED ENDPOINTS"; - static final String END_MARKER = "<!-- END GENERATED ENDPOINTS -->"; - - /** The prefix under which the REST API is exposed, relative to the root of the Openfire admin console. */ - static final String CONTEXT_ROOT = "/plugins"; - - /** Order in which tags are documented. Tags that are not listed here are documented after these, alphabetically. */ - static final List<String> TAG_ORDER = List.of("Users", "User Group", "Chat service", "Chat room", "Client Sessions", "Message", "Message Archive", "Security Audit Log", "Statistics", "System", "Clustering"); - - /** Responses that apply to (nearly) all endpoints. These are documented once, instead of for each endpoint. */ - static final Map<String, String> GENERIC_RESPONSES = Map.of( - "401", "Web service authentication failed.", - "500", "Unexpected, generic error condition." - ); - - static final List<PathItem.HttpMethod> METHOD_ORDER = List.of(PathItem.HttpMethod.GET, PathItem.HttpMethod.POST, PathItem.HttpMethod.PUT, PathItem.HttpMethod.PATCH, PathItem.HttpMethod.DELETE, PathItem.HttpMethod.HEAD, PathItem.HttpMethod.OPTIONS, PathItem.HttpMethod.TRACE); - - record Endpoint(String path, PathItem.HttpMethod method, Operation operation) {} - - public static void main(String[] args) throws Exception - { - if (args.length != 2) { - System.err.println("Usage: java ReadmeEndpointsGenerator.java <openapi.json> <readme.md>"); - System.exit(1); - } - final Path specFile = Path.of(args[0]); - final Path readmeFile = Path.of(args[1]); - - final OpenAPI openAPI = Json.mapper().readValue(specFile.toFile(), OpenAPI.class); - final String generated = generate(openAPI); - - final String readme = Files.readString(readmeFile, StandardCharsets.UTF_8); - final int begin = readme.indexOf(BEGIN_MARKER); - final int end = readme.indexOf(END_MARKER); - if (begin < 0 || end < begin) { - throw new IllegalStateException("Unable to find the '" + BEGIN_MARKER + "' and '" + END_MARKER + "' markers in " + readmeFile); - } - final int beginLineEnd = readme.indexOf('\n', begin) + 1; - final String updated = readme.substring(0, beginLineEnd) + "\n" + generated.strip() + "\n\n" + readme.substring(end); - - if (updated.equals(readme)) { - System.out.println("Endpoint documentation in " + readmeFile + " is up to date."); - } else { - Files.writeString(readmeFile, updated, StandardCharsets.UTF_8); - System.out.println("Updated the endpoint documentation in " + readmeFile + "."); - } - } - - static String generate(final OpenAPI openAPI) - { - // Group all endpoints by their (first) tag. - final Map<String, List<Endpoint>> endpointsByTag = new TreeMap<>(Comparator.comparingInt((String tag) -> TAG_ORDER.contains(tag) ? TAG_ORDER.indexOf(tag) : TAG_ORDER.size()).thenComparing(Comparator.naturalOrder())); - openAPI.getPaths().forEach((path, pathItem) -> pathItem.readOperationsMap().forEach((method, operation) -> { - final String tag = operation.getTags() == null || operation.getTags().isEmpty() ? "Other" : operation.getTags().get(0); - endpointsByTag.computeIfAbsent(tag, t -> new ArrayList<>()).add(new Endpoint(path, method, operation)); - })); - - final Map<String, String> tagDescriptions = new LinkedHashMap<>(); - if (openAPI.getTags() != null) { - for (final Tag tag : openAPI.getTags()) { - tagDescriptions.put(tag.getName(), tag.getDescription()); - } - } - - final StringBuilder out = new StringBuilder(); - out.append("# REST Endpoints\n\n"); - out.append("The paths of all endpoints below are relative to the root of the Openfire admin console, for example `http://example.org:9090`.\n\n"); - out.append("In addition to the responses that are documented for each endpoint, every endpoint can respond with:\n\n"); - GENERIC_RESPONSES.entrySet().stream().sorted(Map.Entry.comparingByKey()).forEach(e -> out.append("- `").append(e.getKey()).append("`: ").append(e.getValue()).append("\n")); - out.append("\n"); - out.append("Interactive documentation of these endpoints is available in the Openfire admin console, via the link on the REST API settings page (Server > Server Settings > REST API).\n"); - - endpointsByTag.forEach((tag, endpoints) -> { - out.append("\n# ").append(tag).append("\n\n"); - final String tagDescription = tagDescriptions.get(tag); - if (tagDescription != null && !tagDescription.isBlank()) { - out.append(tagDescription.trim()).append("\n"); - } - - // Keep endpoints for the same resource (sharing the first path segment after the API prefix) together, and - // document resources that have the most general (shortest) paths first. - final Map<String, Integer> resourceDepth = new TreeMap<>(); - endpoints.forEach(e -> resourceDepth.merge(resource(e.path()), depth(e.path()), Math::min)); - endpoints.sort(Comparator.comparingInt((Endpoint e) -> resourceDepth.get(resource(e.path()))) - .thenComparing(e -> resource(e.path())) - .thenComparing(Endpoint::path) - .thenComparingInt(e -> METHOD_ORDER.indexOf(e.method()))); - for (final Endpoint endpoint : endpoints) { - appendEndpoint(out, endpoint); - } - }); - return out.toString().replaceAll("\n{3,}", "\n\n"); - } - - static void appendEndpoint(final StringBuilder out, final Endpoint endpoint) - { - final Operation operation = endpoint.operation(); - out.append("\n## ").append(Optional.ofNullable(operation.getSummary()).orElse(endpoint.method() + " " + endpoint.path())).append("\n\n"); - out.append("> **").append(endpoint.method()).append("** ").append(CONTEXT_ROOT).append(endpoint.path()).append("\n\n"); - if (Boolean.TRUE.equals(operation.getDeprecated())) { - out.append("**Deprecated:** this endpoint may be removed in a future version.\n\n"); - } - if (operation.getDescription() != null && !operation.getDescription().isBlank()) { - out.append(operation.getDescription().trim()).append("\n\n"); - } - - final List<Parameter> parameters = Optional.ofNullable(operation.getParameters()).orElse(List.of()); - if (!parameters.isEmpty()) { - out.append("**Parameters**\n\n"); - out.append("| Name | Located in | Required | Description | Default value |\n"); - out.append("|------|------------|----------|-------------|---------------|\n"); - for (final Parameter parameter : parameters) { - String description = Optional.ofNullable(parameter.getDescription()).orElse(""); - if (parameter.getExample() != null) { - description = (description.isBlank() ? "" : description.trim() + " ") + "Example: `" + parameter.getExample() + "`"; - } else if (parameter.getExamples() != null && !parameter.getExamples().isEmpty()) { - final List<String> examples = new ArrayList<>(); - parameter.getExamples().values().forEach(example -> examples.add("`" + example.getValue() + "`" + (example.getDescription() == null || example.getDescription().isBlank() ? "" : " (" + example.getDescription().trim() + ")"))); - description = (description.isBlank() ? "" : description.trim() + " ") + "Examples: " + String.join(", ", examples); - } - final Object defaultValue = parameter.getSchema() != null ? parameter.getSchema().getDefault() : null; - out.append("| ").append(parameter.getName()) - .append(" | ").append(parameter.getIn()) - .append(" | ").append(Boolean.TRUE.equals(parameter.getRequired()) ? "yes" : "no") - .append(" | ").append(cell(description)) - .append(" | ").append(defaultValue == null ? "" : "`" + cell(defaultValue.toString()) + "`") - .append(" |\n"); - } - out.append("\n"); - } - - final RequestBody requestBody = operation.getRequestBody(); - if (requestBody != null) { - out.append("**Request body**"); - out.append(Boolean.TRUE.equals(requestBody.getRequired()) ? " (required)" : " (optional)"); - out.append(": ").append(describeContent(requestBody.getContent())); - if (requestBody.getDescription() != null && !requestBody.getDescription().isBlank()) { - out.append(" - ").append(requestBody.getDescription().trim()); - } - out.append("\n\n"); - } - - if (operation.getResponses() != null) { - final Map<String, ApiResponse> responses = new TreeMap<>(operation.getResponses()); - responses.entrySet().removeIf(e -> Objects.equals(GENERIC_RESPONSES.get(e.getKey()), e.getValue().getDescription())); - if (!responses.isEmpty()) { - out.append("**Responses**\n\n"); - out.append("| Status | Description | Response body |\n"); - out.append("|--------|-------------|---------------|\n"); - responses.forEach((status, response) -> out.append("| ").append(status) - .append(" | ").append(cell(Optional.ofNullable(response.getDescription()).orElse(""))) - .append(" | ").append(response.getContent() == null || response.getContent().isEmpty() ? "" : describeContent(response.getContent())) - .append(" |\n")); - out.append("\n"); - } - } - } - - /** Describes the data type of content, for example "`UserEntity` (XML or JSON)". */ - static String describeContent(final Content content) - { - if (content == null || content.isEmpty()) { - return "none"; - } - final List<String> formats = new ArrayList<>(); - String type = null; - for (final Map.Entry<String, MediaType> entry : content.entrySet()) { - final String format = switch (entry.getKey()) { - case "application/xml" -> "XML"; - case "application/json" -> "JSON"; - case "text/plain" -> "plain text"; - case "*/*" -> null; - default -> entry.getKey(); - }; - if (format != null) { - formats.add(format); - } - if (type == null && entry.getValue().getSchema() != null) { - type = describeSchema(entry.getValue().getSchema()); - } - } - final StringBuilder result = new StringBuilder(type == null ? "unspecified" : type); - if (!formats.isEmpty()) { - result.append(" (").append(String.join(" or ", formats)).append(")"); - } - return result.toString(); - } - - @SuppressWarnings("rawtypes") - static String describeSchema(final Schema schema) - { - if (schema.get$ref() != null) { - return "`" + schema.get$ref().substring(schema.get$ref().lastIndexOf('/') + 1) + "`"; - } - if ("array".equals(schema.getType()) && schema.getItems() != null) { - return "array of " + describeSchema(schema.getItems()); - } - return schema.getType() == null ? "unspecified" : schema.getType(); - } - - /** Returns the path without the API prefix, for example "users/{username}" for "/restapi/v1/users/{username}". */ - static String relativePath(final String path) - { - return path.replaceFirst("^/restapi/v\\d+/", ""); - } - - /** Returns the name of the resource that is addressed by a path, for example "users" for "/restapi/v1/users/{username}". */ - static String resource(final String path) - { - return relativePath(path).split("/")[0]; - } - - /** Returns the number of path segments of a path, excluding the API prefix. */ - static int depth(final String path) - { - return relativePath(path).split("/").length; - } - - /** Makes text safe for use in a markdown table cell. */ - static String cell(final String text) - { - return text.trim().replace("|", "\\|").replaceAll("\\s*\\R\\s*", "<br>"); - } -} diff --git a/src/build/ReadmeGenerator.java b/src/build/ReadmeGenerator.java new file mode 100644 index 000000000..9f72a6be9 --- /dev/null +++ b/src/build/ReadmeGenerator.java @@ -0,0 +1,571 @@ +/* + * Copyright (C) 2026 Ignite Realtime Foundation. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.introspect.BeanPropertyDefinition; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.JsonNodeFactory; +import com.fasterxml.jackson.databind.node.ObjectNode; +import io.swagger.v3.core.util.Json; +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.Operation; +import io.swagger.v3.oas.models.PathItem; +import io.swagger.v3.oas.models.media.Content; +import io.swagger.v3.oas.models.media.MediaType; +import io.swagger.v3.oas.models.media.Schema; +import io.swagger.v3.oas.models.parameters.Parameter; +import io.swagger.v3.oas.models.parameters.RequestBody; +import io.swagger.v3.oas.models.responses.ApiResponse; +import io.swagger.v3.oas.models.tags.Tag; +import org.jivesoftware.openfire.plugin.rest.CustomJacksonMapperProvider; + +import java.io.StringWriter; +import java.lang.reflect.Modifier; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.TimeZone; +import java.util.TreeMap; +import java.util.TreeSet; +import java.util.stream.Collectors; + +import javax.xml.bind.JAXBContext; +import javax.xml.bind.Marshaller; + +/** + * Generates the documentation of the endpoints and data types in readme.md from the OpenAPI specification, which in + * turn is generated from the OpenAPI annotations in the source code. This keeps the documentation in sync with the + * implementation. + * + * The generated documentation replaces all text between pairs of marker lines in the readme, like + * {@code <!-- BEGIN GENERATED ENDPOINTS ... -->} and {@code <!-- END GENERATED ENDPOINTS -->}. Text outside of these + * markers is not modified. + * + * Example request bodies are generated from the examples in the specification, and are then converted into the entity + * classes of the plugin and back, using the same JSON and XML serialization as the plugin. This guarantees that the + * examples use the actual format of the REST API: when an example cannot be processed, generation fails. + * + * This is a single-file Java program that is executed by the Maven build. It is not part of the plugin. + * + * Usage: {@code java -cp <classpath> ReadmeGenerator.java <openapi.json> <readme.md>} + */ +public class ReadmeGenerator +{ + /** The prefix under which the REST API is exposed, relative to the root of the Openfire admin console. */ + static final String CONTEXT_ROOT = "/plugins"; + + /** Order in which tags are documented. Tags that are not listed here are documented after these, alphabetically. */ + static final List<String> TAG_ORDER = List.of("Users", "User Group", "Chat service", "Chat room", "Client Sessions", "Message", "Message Archive", "Security Audit Log", "Statistics", "System", "Clustering"); + + /** Responses that apply to (nearly) all endpoints. These are documented once, instead of for each endpoint. */ + static final Map<String, String> GENERIC_RESPONSES = Map.of( + "401", "Web service authentication failed.", + "500", "Unexpected, generic error condition." + ); + + static final List<PathItem.HttpMethod> METHOD_ORDER = List.of(PathItem.HttpMethod.GET, PathItem.HttpMethod.POST, PathItem.HttpMethod.PUT, PathItem.HttpMethod.PATCH, PathItem.HttpMethod.DELETE, PathItem.HttpMethod.HEAD, PathItem.HttpMethod.OPTIONS, PathItem.HttpMethod.TRACE); + + /** Packages that contain the classes that are used as request and response bodies. */ + static final List<String> ENTITY_PACKAGES = List.of("org.jivesoftware.openfire.plugin.rest.entity", "org.jivesoftware.openfire.plugin.rest.exceptions"); + + /** The value used in examples for date/time values, which do not have an example defined in the annotations. */ + static final String EXAMPLE_DATE_TIME = "2026-01-31T12:34:56.789Z"; + + record Endpoint(String path, PathItem.HttpMethod method, Operation operation) {} + + final OpenAPI openAPI; + final ObjectMapper mapper = new CustomJacksonMapperProvider().getContext(Object.class); + + ReadmeGenerator(final OpenAPI openAPI) + { + this.openAPI = openAPI; + } + + public static void main(String[] args) throws Exception + { + if (args.length != 2) { + System.err.println("Usage: java ReadmeGenerator.java <openapi.json> <readme.md>"); + System.exit(1); + } + final Path specFile = Path.of(args[0]); + final Path readmeFile = Path.of(args[1]); + + // Make the generated output independent of the environment that the build runs in. + TimeZone.setDefault(TimeZone.getTimeZone("UTC")); + Locale.setDefault(Locale.ROOT); + + final ReadmeGenerator generator = new ReadmeGenerator(Json.mapper().readValue(specFile.toFile(), OpenAPI.class)); + generator.verifyPropertyNames(); + + final String readme = Files.readString(readmeFile, StandardCharsets.UTF_8); + String updated = replaceSection(readme, "ENDPOINTS", generator.generateEndpoints()); + updated = replaceSection(updated, "DATA TYPES", generator.generateDataTypes()); + + if (updated.equals(readme)) { + System.out.println("Generated documentation in " + readmeFile + " is up to date."); + } else { + Files.writeString(readmeFile, updated, StandardCharsets.UTF_8); + System.out.println("Updated the generated documentation in " + readmeFile + "."); + } + } + + /** Replaces the text between the BEGIN and END markers of a section with new content. */ + static String replaceSection(final String readme, final String section, final String content) + { + final String beginMarker = "<!-- BEGIN GENERATED " + section; + final String endMarker = "<!-- END GENERATED " + section + " -->"; + final int begin = readme.indexOf(beginMarker); + final int end = readme.indexOf(endMarker); + if (begin < 0 || end < begin) { + throw new IllegalStateException("Unable to find the '" + beginMarker + "' and '" + endMarker + "' markers in the readme."); + } + final int beginLineEnd = readme.indexOf('\n', begin) + 1; + return readme.substring(0, beginLineEnd) + "\n" + content.strip().replaceAll("\n{3,}", "\n\n") + "\n\n" + readme.substring(end); + } + + // --------------------------------------------------------------------------------------------------------------- + // Endpoints + // --------------------------------------------------------------------------------------------------------------- + + String generateEndpoints() throws Exception + { + // Group all endpoints by their (first) tag. + final Map<String, List<Endpoint>> endpointsByTag = new TreeMap<>(Comparator.comparingInt((String tag) -> TAG_ORDER.contains(tag) ? TAG_ORDER.indexOf(tag) : TAG_ORDER.size()).thenComparing(Comparator.naturalOrder())); + openAPI.getPaths().forEach((path, pathItem) -> pathItem.readOperationsMap().forEach((method, operation) -> { + final String tag = operation.getTags() == null || operation.getTags().isEmpty() ? "Other" : operation.getTags().get(0); + endpointsByTag.computeIfAbsent(tag, t -> new ArrayList<>()).add(new Endpoint(path, method, operation)); + })); + + final Map<String, String> tagDescriptions = new LinkedHashMap<>(); + if (openAPI.getTags() != null) { + for (final Tag tag : openAPI.getTags()) { + tagDescriptions.put(tag.getName(), tag.getDescription()); + } + } + + final StringBuilder out = new StringBuilder(); + out.append("# REST Endpoints\n\n"); + out.append("The paths of all endpoints below are relative to the root of the Openfire admin console, for example `http://example.org:9090`. The data types that are used by these endpoints are described in [Data types](#data-types).\n\n"); + out.append("In addition to the responses that are documented for each endpoint, every endpoint can respond with:\n\n"); + GENERIC_RESPONSES.entrySet().stream().sorted(Map.Entry.comparingByKey()).forEach(e -> out.append("- `").append(e.getKey()).append("`: ").append(e.getValue()).append("\n")); + out.append("\n"); + out.append("Interactive documentation of these endpoints is available in the Openfire admin console, via the link on the REST API settings page (Server > Server Settings > REST API).\n"); + + for (final Map.Entry<String, List<Endpoint>> entry : endpointsByTag.entrySet()) { + final String tag = entry.getKey(); + final List<Endpoint> endpoints = entry.getValue(); + out.append("\n# ").append(tag).append("\n\n"); + final String tagDescription = tagDescriptions.get(tag); + if (tagDescription != null && !tagDescription.isBlank()) { + out.append(tagDescription.trim()).append("\n"); + } + + // Keep endpoints for the same resource (sharing the first path segment after the API prefix) together, and + // document resources that have the most general (shortest) paths first. + final Map<String, Integer> resourceDepth = new TreeMap<>(); + endpoints.forEach(e -> resourceDepth.merge(resource(e.path()), depth(e.path()), Math::min)); + endpoints.sort(Comparator.comparingInt((Endpoint e) -> resourceDepth.get(resource(e.path()))) + .thenComparing(e -> resource(e.path())) + .thenComparing(Endpoint::path) + .thenComparingInt(e -> METHOD_ORDER.indexOf(e.method()))); + for (final Endpoint endpoint : endpoints) { + appendEndpoint(out, endpoint); + } + } + return out.toString(); + } + + void appendEndpoint(final StringBuilder out, final Endpoint endpoint) throws Exception + { + final Operation operation = endpoint.operation(); + out.append("\n## ").append(Optional.ofNullable(operation.getSummary()).orElse(endpoint.method() + " " + endpoint.path())).append("\n\n"); + out.append("> **").append(endpoint.method()).append("** ").append(CONTEXT_ROOT).append(endpoint.path()).append("\n\n"); + if (Boolean.TRUE.equals(operation.getDeprecated())) { + out.append("**Deprecated:** this endpoint may be removed in a future version.\n\n"); + } + if (operation.getDescription() != null && !operation.getDescription().isBlank()) { + out.append(operation.getDescription().trim()).append("\n\n"); + } + + final List<Parameter> parameters = Optional.ofNullable(operation.getParameters()).orElse(List.of()); + if (!parameters.isEmpty()) { + out.append("**Parameters**\n\n"); + out.append("| Name | Located in | Required | Description | Default value |\n"); + out.append("|------|------------|----------|-------------|---------------|\n"); + for (final Parameter parameter : parameters) { + String description = Optional.ofNullable(parameter.getDescription()).orElse(""); + if (parameter.getExample() != null) { + description = (description.isBlank() ? "" : description.trim() + " ") + "Example: `" + parameter.getExample() + "`"; + } else if (parameter.getExamples() != null && !parameter.getExamples().isEmpty()) { + final List<String> examples = new ArrayList<>(); + parameter.getExamples().values().forEach(example -> examples.add("`" + example.getValue() + "`" + (example.getDescription() == null || example.getDescription().isBlank() ? "" : " (" + example.getDescription().trim() + ")"))); + description = (description.isBlank() ? "" : description.trim() + " ") + "Examples: " + String.join(", ", examples); + } + final Object defaultValue = parameter.getSchema() != null ? parameter.getSchema().getDefault() : null; + out.append("| ").append(parameter.getName()) + .append(" | ").append(parameter.getIn()) + .append(" | ").append(Boolean.TRUE.equals(parameter.getRequired()) ? "yes" : "no") + .append(" | ").append(cell(description)) + .append(" | ").append(defaultValue == null ? "" : "`" + cell(defaultValue.toString()) + "`") + .append(" |\n"); + } + out.append("\n"); + } + + final RequestBody requestBody = operation.getRequestBody(); + if (requestBody != null) { + out.append("**Request body**"); + out.append(Boolean.TRUE.equals(requestBody.getRequired()) ? " (required)" : " (optional)"); + out.append(": ").append(describeContent(requestBody.getContent())); + if (requestBody.getDescription() != null && !requestBody.getDescription().isBlank()) { + out.append(" - ").append(requestBody.getDescription().trim()); + } + out.append("\n\n"); + appendExamples(out, requestBody.getContent()); + } + + if (operation.getResponses() != null) { + final Map<String, ApiResponse> responses = new TreeMap<>(operation.getResponses()); + responses.entrySet().removeIf(e -> Objects.equals(GENERIC_RESPONSES.get(e.getKey()), e.getValue().getDescription())); + if (!responses.isEmpty()) { + out.append("**Responses**\n\n"); + out.append("| Status | Description | Response body |\n"); + out.append("|--------|-------------|---------------|\n"); + responses.forEach((status, response) -> out.append("| ").append(status) + .append(" | ").append(cell(Optional.ofNullable(response.getDescription()).orElse(""))) + .append(" | ").append(response.getContent() == null || response.getContent().isEmpty() ? "" : describeContent(response.getContent())) + .append(" |\n")); + out.append("\n"); + } + } + } + + /** Appends example XML and JSON representations of content, if the content is an entity of the plugin. */ + void appendExamples(final StringBuilder out, final Content content) throws Exception + { + if (content == null) { + return; + } + + // Use examples that are explicitly defined for the content, when available. + final Map<String, String> explicitExamples = new LinkedHashMap<>(); + content.forEach((mediaType, value) -> { + if (value.getExample() != null) { + explicitExamples.put(mediaType, value.getExample().toString()); + } else if (value.getExamples() != null && !value.getExamples().isEmpty()) { + explicitExamples.put(mediaType, String.valueOf(value.getExamples().values().iterator().next().getValue())); + } + }); + if (!explicitExamples.isEmpty()) { + out.append("<details>\n<summary>Example request body</summary>\n\n"); + explicitExamples.forEach((mediaType, example) -> { + final String language = mediaType.endsWith("xml") ? "xml" : mediaType.endsWith("json") ? "json" : ""; + out.append("`Content-Type: ").append(mediaType).append("`:\n\n```").append(language).append("\n").append(example.strip()).append("\n```\n\n"); + }); + out.append("</details>\n\n"); + return; + } + + final Schema<?> schema = content.values().stream().map(MediaType::getSchema).filter(Objects::nonNull).findFirst().orElse(null); + if (schema == null || schema.get$ref() == null) { + return; + } + final Class<?> entityClass = findEntityClass(refName(schema.get$ref())); + if (entityClass == null || Modifier.isAbstract(entityClass.getModifiers())) { + return; + } + + // Convert the example into an instance of the entity class, then serialize that instance like the plugin does. + final Object entity = mapper.treeToValue(example(schema, 0), entityClass); + + out.append("<details>\n<summary>Example request bodies</summary>\n\n"); + if (content.containsKey("application/xml")) { + final Marshaller marshaller = JAXBContext.newInstance(entityClass).createMarshaller(); + marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true); + marshaller.setProperty(Marshaller.JAXB_FRAGMENT, true); + final StringWriter xml = new StringWriter(); + marshaller.marshal(entity, xml); + out.append("XML (`Content-Type: application/xml`):\n\n```xml\n").append(xml.toString().strip()).append("\n```\n\n"); + } + if (content.containsKey("application/json")) { + out.append("JSON (`Content-Type: application/json`):\n\n```json\n").append(mapper.writerWithDefaultPrettyPrinter().writeValueAsString(entity).strip()).append("\n```\n\n"); + } + out.append("</details>\n\n"); + } + + /** Builds an example JSON value for a schema, based on the examples that are defined in the specification. */ + JsonNode example(final Schema<?> schema, final int depth) + { + if (depth > 10) { + throw new IllegalStateException("Schema nesting is too deep (recursive schema?)"); + } + if (schema.get$ref() != null) { + return example(resolve(schema.get$ref()), depth + 1); + } + if (schema.getExample() != null) { + return mapper.valueToTree(schema.getExample()); + } + if ("array".equals(schema.getType()) && schema.getItems() != null) { + final ArrayNode array = JsonNodeFactory.instance.arrayNode(); + array.add(example(schema.getItems(), depth + 1)); + return array; + } + if (schema.getProperties() != null) { + final ObjectNode object = JsonNodeFactory.instance.objectNode(); + schema.getProperties().forEach((name, property) -> object.set(name, example(property, depth + 1))); + return object; + } + if ("date-time".equals(schema.getFormat())) { + return JsonNodeFactory.instance.textNode(EXAMPLE_DATE_TIME); + } + if (schema.getEnum() != null && !schema.getEnum().isEmpty()) { + return mapper.valueToTree(schema.getEnum().get(0)); + } + return switch (Optional.ofNullable(schema.getType()).orElse("")) { + case "integer", "number" -> JsonNodeFactory.instance.numberNode(0); + case "boolean" -> JsonNodeFactory.instance.booleanNode(false); + default -> JsonNodeFactory.instance.textNode("string"); + }; + } + + /** + * Verifies that the names of the properties in the specification are the names that are used in JSON. These can + * differ, as JSON serialization uses JAXB annotations when there are no Jackson annotations, which the generator of + * the specification does not do. The difference is fixed by adding a Jackson annotation (like + * {@code @JsonProperty}) that uses the name that is used in JSON. + */ + @SuppressWarnings({"rawtypes", "unchecked"}) + void verifyPropertyNames() + { + final List<String> problems = new ArrayList<>(); + final Map<String, Schema> schemas = new TreeMap<>(Optional.ofNullable(openAPI.getComponents().getSchemas()).orElse(Map.of())); + schemas.forEach((name, schema) -> { + final Class<?> entityClass = findEntityClass(name); + if (entityClass == null || schema.getProperties() == null) { + return; + } + final Set<String> jsonNames = mapper.getSerializationConfig().introspect(mapper.constructType(entityClass)).findProperties().stream() + .filter(BeanPropertyDefinition::couldSerialize) + .map(BeanPropertyDefinition::getName) + .collect(Collectors.toSet()); + final Set<String> specNames = new TreeSet<String>(schema.getProperties().keySet()); + specNames.removeAll(jsonNames); + if (!specNames.isEmpty()) { + problems.add(name + ": " + specNames + " (names used in JSON: " + new TreeSet<>(jsonNames) + ")"); + } + }); + if (!problems.isEmpty()) { + throw new IllegalStateException("The OpenAPI specification uses property names that are not used in JSON. Add a @JsonProperty annotation with the name that is used in JSON to:\n" + String.join("\n", problems)); + } + } + + static Class<?> findEntityClass(final String name) + { + for (final String pkg : ENTITY_PACKAGES) { + try { + return Class.forName(pkg + "." + name); + } catch (ClassNotFoundException e) { + // Try the next package. + } + } + return null; + } + + // --------------------------------------------------------------------------------------------------------------- + // Data types + // --------------------------------------------------------------------------------------------------------------- + + @SuppressWarnings({"rawtypes", "unchecked"}) + String generateDataTypes() + { + final StringBuilder out = new StringBuilder(); + out.append("## Data types\n\n"); + out.append("These are the data types that are used in the request and response bodies of the endpoints. The name of a field is the name that is used in JSON. When XML uses a different name, it is mentioned in the description of the field.\n\n"); + out.append("Date/time values are represented as an ISO-8601 formatted text in XML (for example: `" + EXAMPLE_DATE_TIME + "`), and as the number of milliseconds since the Unix epoch in JSON (for example: `1769862896789`). In JSON request bodies, the ISO-8601 format can also be used.\n"); + + final Map<String, Schema> schemas = new TreeMap<>(Optional.ofNullable(openAPI.getComponents().getSchemas()).orElse(Map.of())); + schemas.forEach((name, schema) -> { + out.append("\n### ").append(name).append("\n\n"); + if (schema.getDescription() != null && !schema.getDescription().isBlank()) { + out.append(schema.getDescription().trim()).append("\n\n"); + } + if (schema.getXml() != null && schema.getXml().getName() != null) { + out.append("XML root element: `<").append(schema.getXml().getName()).append(">`\n\n"); + } + final Map<String, Schema> properties = schema.getProperties(); + if (properties == null || properties.isEmpty()) { + return; + } + final List<String> required = Optional.ofNullable((List<String>) schema.getRequired()).orElse(List.of()); + out.append("| Field | Type | Required | Description |\n"); + out.append("|-------|------|----------|-------------|\n"); + properties.forEach((propertyName, property) -> out.append("| ").append(propertyName) + .append(" | ").append(describeSchema(property)) + .append(" | ").append(required.contains(propertyName) ? "yes" : "no") + .append(" | ").append(cell(describeProperty(propertyName, property))) + .append(" |\n")); + }); + return out.toString(); + } + + /** Describes a property of a data type, including its XML representation, allowed values and example. */ + @SuppressWarnings("rawtypes") + static String describeProperty(final String propertyName, final Schema property) + { + final List<String> parts = new ArrayList<>(); + if (property.getDescription() != null && !property.getDescription().isBlank()) { + parts.add(sentence(property.getDescription())); + } + final List<?> allowed = property.getEnum() != null ? property.getEnum() : (property.getItems() != null ? property.getItems().getEnum() : null); + if (allowed != null && !allowed.isEmpty()) { + parts.add("Allowed values: " + allowed.stream().map(v -> "`" + v + "`").collect(Collectors.joining(", ")) + "."); + } + final String xml = describeXml(propertyName, property); + if (xml != null) { + parts.add(xml); + } + final Object example = property.getExample() != null ? property.getExample() : (property.getItems() != null ? property.getItems().getExample() : null); + if (example != null) { + parts.add("Example: `" + example + "`"); + } + return String.join(" ", parts); + } + + /** Describes how a property is represented in XML, when that differs from its (JSON) name. */ + @SuppressWarnings("rawtypes") + static String describeXml(final String propertyName, final Schema property) + { + final String xmlName = property.getXml() != null && property.getXml().getName() != null ? property.getXml().getName() : propertyName; + if (property.getXml() != null && Boolean.TRUE.equals(property.getXml().getAttribute())) { + return "In XML, this is the `" + xmlName + "` attribute."; + } + if ("array".equals(property.getType()) && property.getItems() != null) { + final Schema items = property.getItems(); + final String itemName = items.getXml() != null && items.getXml().getName() != null ? items.getXml().getName() : null; + if (property.getXml() != null && Boolean.TRUE.equals(property.getXml().getWrapped())) { + return itemName == null ? "In XML, the items are wrapped in the `<" + xmlName + ">` element." : "In XML, items are represented as `<" + itemName + ">` elements, wrapped in the `<" + xmlName + ">` element."; + } + return "In XML, items are represented as `<" + xmlName + ">` elements."; + } + return xmlName.equals(propertyName) ? null : "In XML, this is the `<" + xmlName + ">` element."; + } + + /** Returns text as a sentence that ends with a punctuation mark. */ + static String sentence(final String text) + { + final String trimmed = text.trim(); + return trimmed.matches(".*[.!?:]$") ? trimmed : trimmed + "."; + } + + // --------------------------------------------------------------------------------------------------------------- + // Utilities + // --------------------------------------------------------------------------------------------------------------- + + Schema<?> resolve(final String ref) + { + final Schema<?> schema = openAPI.getComponents().getSchemas().get(refName(ref)); + if (schema == null) { + throw new IllegalStateException("Unable to resolve schema reference: " + ref); + } + return schema; + } + + static String refName(final String ref) + { + return ref.substring(ref.lastIndexOf('/') + 1); + } + + /** Describes the data type of content, for example "`UserEntity` (XML or JSON)". */ + static String describeContent(final Content content) + { + if (content == null || content.isEmpty()) { + return "none"; + } + final List<String> formats = new ArrayList<>(); + String type = null; + for (final Map.Entry<String, MediaType> entry : content.entrySet()) { + final String format = switch (entry.getKey()) { + case "application/xml" -> "XML"; + case "application/json" -> "JSON"; + case "text/plain" -> "plain text"; + case "*/*" -> null; + default -> entry.getKey(); + }; + if (format != null) { + formats.add(format); + } + if (type == null && entry.getValue().getSchema() != null) { + type = describeSchema(entry.getValue().getSchema()); + } + } + final StringBuilder result = new StringBuilder(type == null ? "unspecified" : type); + if (!formats.isEmpty()) { + result.append(" (").append(String.join(" or ", formats)).append(")"); + } + return result.toString(); + } + + /** Describes the type of a schema, linking to the documentation of data types. */ + @SuppressWarnings("rawtypes") + static String describeSchema(final Schema schema) + { + if (schema.get$ref() != null) { + final String name = refName(schema.get$ref()); + return "[" + name + "](#" + name.toLowerCase(Locale.ROOT) + ")"; + } + if ("array".equals(schema.getType()) && schema.getItems() != null) { + return "array of " + describeSchema(schema.getItems()); + } + if (schema.getFormat() != null && "date-time".equals(schema.getFormat())) { + return "date-time"; + } + return schema.getType() == null ? "unspecified" : schema.getType(); + } + + /** Returns the path without the API prefix, for example "users/{username}" for "/restapi/v1/users/{username}". */ + static String relativePath(final String path) + { + return path.replaceFirst("^/restapi/v\\d+/", ""); + } + + /** Returns the name of the resource that is addressed by a path, for example "users" for "/restapi/v1/users/{username}". */ + static String resource(final String path) + { + return relativePath(path).split("/")[0]; + } + + /** Returns the number of path segments of a path, excluding the API prefix. */ + static int depth(final String path) + { + return relativePath(path).split("/").length; + } + + /** Makes text safe for use in a markdown table cell. */ + static String cell(final String text) + { + return text.trim().replace("|", "\\|").replaceAll("\\s*\\R\\s*", "<br>"); + } +} diff --git a/src/java/org/jivesoftware/openfire/plugin/rest/entity/AdminEntities.java b/src/java/org/jivesoftware/openfire/plugin/rest/entity/AdminEntities.java index 44620a9aa..df9d880f0 100644 --- a/src/java/org/jivesoftware/openfire/plugin/rest/entity/AdminEntities.java +++ b/src/java/org/jivesoftware/openfire/plugin/rest/entity/AdminEntities.java @@ -17,12 +17,15 @@ package org.jivesoftware.openfire.plugin.rest.entity; import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.ArraySchema; +import io.swagger.v3.oas.annotations.media.Schema; import javax.xml.bind.annotation.XmlElement; import javax.xml.bind.annotation.XmlRootElement; import java.util.List; @XmlRootElement(name = "admins") +@Schema(description = "A list of entities that have an admin affiliation with a multi-user chat room.") public class AdminEntities extends AffiliatedEntities { List<String> admins; @@ -36,6 +39,7 @@ public AdminEntities(List<String> admins) { @XmlElement(name = "admin") @JsonProperty(value = "admins") + @ArraySchema(arraySchema = @Schema(description = "The JIDs (or names of local users) of the entities."), schema = @Schema(example = "jane@example.org")) public List<String> getAdmins() { return admins; } diff --git a/src/java/org/jivesoftware/openfire/plugin/rest/entity/AffiliatedEntities.java b/src/java/org/jivesoftware/openfire/plugin/rest/entity/AffiliatedEntities.java index 282bad741..1d406554b 100644 --- a/src/java/org/jivesoftware/openfire/plugin/rest/entity/AffiliatedEntities.java +++ b/src/java/org/jivesoftware/openfire/plugin/rest/entity/AffiliatedEntities.java @@ -15,6 +15,8 @@ */ package org.jivesoftware.openfire.plugin.rest.entity; +import io.swagger.v3.oas.annotations.media.Schema; + /** * A base class for pre-existing classes that each represent a collection of MUC-room affiliated users of a specific * type. @@ -24,6 +26,7 @@ * * @author Guus der Kinderen, guus@goodbytes.nl */ +@Schema(description = "A list of entities that have a particular affiliation with a multi-user chat room. Depending on the affiliation, this is an AdminEntities, MemberEntities, OutcastEntities or OwnerEntities value.") public abstract class AffiliatedEntities { /** diff --git a/src/java/org/jivesoftware/openfire/plugin/rest/entity/ClusterNodeEntities.java b/src/java/org/jivesoftware/openfire/plugin/rest/entity/ClusterNodeEntities.java index 60f9d7c15..f6a0309d2 100644 --- a/src/java/org/jivesoftware/openfire/plugin/rest/entity/ClusterNodeEntities.java +++ b/src/java/org/jivesoftware/openfire/plugin/rest/entity/ClusterNodeEntities.java @@ -16,6 +16,8 @@ package org.jivesoftware.openfire.plugin.rest.entity; import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.ArraySchema; +import io.swagger.v3.oas.annotations.media.Schema; import javax.annotation.Nonnull; import javax.xml.bind.annotation.XmlElement; @@ -23,6 +25,7 @@ import java.util.List; @XmlRootElement(name = "clusterNodes") +@Schema(description = "A list of the nodes in an Openfire cluster.") public class ClusterNodeEntities { private List<ClusterNodeEntity> clusterNodeEntities; @@ -35,6 +38,7 @@ public ClusterNodeEntities(@Nonnull final List<ClusterNodeEntity> clusterNodeEnt @XmlElement(name = "clusterNode") @JsonProperty(value = "clusterNodes") + @ArraySchema(arraySchema = @Schema(description = "The nodes of the cluster.")) public List<ClusterNodeEntity> getClusterNodeEntities() { return clusterNodeEntities; } diff --git a/src/java/org/jivesoftware/openfire/plugin/rest/entity/ClusterNodeEntity.java b/src/java/org/jivesoftware/openfire/plugin/rest/entity/ClusterNodeEntity.java index 8bb3ccccf..c149ba8f9 100644 --- a/src/java/org/jivesoftware/openfire/plugin/rest/entity/ClusterNodeEntity.java +++ b/src/java/org/jivesoftware/openfire/plugin/rest/entity/ClusterNodeEntity.java @@ -15,6 +15,7 @@ */ package org.jivesoftware.openfire.plugin.rest.entity; +import io.swagger.v3.oas.annotations.media.Schema; import org.jivesoftware.openfire.cluster.ClusterNodeInfo; import org.jivesoftware.openfire.cluster.NodeID; @@ -24,6 +25,7 @@ import java.util.Date; @XmlRootElement(name = "clusterNode") +@Schema(description = "A node in an Openfire cluster.") public class ClusterNodeEntity { private String hostName; @@ -49,6 +51,7 @@ public ClusterNodeEntity(String hostName, NodeID nodeID, long joinedTime, boolea } @XmlElement + @Schema(description = "The host name and IP address of the server on which this cluster node is running.", example = "xmpp1.example.org (192.168.0.10)") public String getHostName() { return hostName; } @@ -58,6 +61,7 @@ public void setHostName(String hostName) { } @XmlElement + @Schema(description = "The unique identifier of this cluster node.", example = "a3f1c2d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d") public String getNodeID() { return nodeID; } @@ -67,6 +71,7 @@ public void setNodeID(String nodeID) { } @XmlElement + @Schema(description = "The moment at which this node joined the cluster.") public Date getJoinedTime() { return joinedTime; } @@ -76,6 +81,7 @@ public void setJoinedTime(Date joinedTime) { } @XmlElement + @Schema(description = "Whether this node currently is the senior member of the cluster.", example = "true") public boolean isSeniorMember() { return seniorMember; } diff --git a/src/java/org/jivesoftware/openfire/plugin/rest/entity/ClusteringEntity.java b/src/java/org/jivesoftware/openfire/plugin/rest/entity/ClusteringEntity.java index 4af75d38b..91b9b6f39 100644 --- a/src/java/org/jivesoftware/openfire/plugin/rest/entity/ClusteringEntity.java +++ b/src/java/org/jivesoftware/openfire/plugin/rest/entity/ClusteringEntity.java @@ -15,12 +15,14 @@ */ package org.jivesoftware.openfire.plugin.rest.entity; + import io.swagger.v3.oas.annotations.media.Schema; import javax.xml.bind.annotation.XmlElement; import javax.xml.bind.annotation.XmlRootElement; @XmlRootElement(name = "clustering") +@Schema(description = "The clustering status of an Openfire instance.") public class ClusteringEntity { String status; diff --git a/src/java/org/jivesoftware/openfire/plugin/rest/entity/GroupEntities.java b/src/java/org/jivesoftware/openfire/plugin/rest/entity/GroupEntities.java index d7f86bc3c..356e01a1d 100644 --- a/src/java/org/jivesoftware/openfire/plugin/rest/entity/GroupEntities.java +++ b/src/java/org/jivesoftware/openfire/plugin/rest/entity/GroupEntities.java @@ -17,6 +17,8 @@ package org.jivesoftware.openfire.plugin.rest.entity; import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.ArraySchema; +import io.swagger.v3.oas.annotations.media.Schema; import java.util.List; @@ -28,6 +30,7 @@ * The Class GroupEntities. */ @XmlRootElement(name = "groups") +@Schema(description = "A list of Openfire user groups.") public class GroupEntities { /** The groups. */ @@ -55,6 +58,7 @@ public GroupEntities(List<GroupEntity> groups) { */ @XmlElement(name = "group") @JsonProperty(value = "groups") + @ArraySchema(arraySchema = @Schema(description = "The groups.")) public List<GroupEntity> getGroups() { return groups; } diff --git a/src/java/org/jivesoftware/openfire/plugin/rest/entity/GroupEntity.java b/src/java/org/jivesoftware/openfire/plugin/rest/entity/GroupEntity.java index 277e3448f..6fcf04c92 100644 --- a/src/java/org/jivesoftware/openfire/plugin/rest/entity/GroupEntity.java +++ b/src/java/org/jivesoftware/openfire/plugin/rest/entity/GroupEntity.java @@ -33,6 +33,7 @@ */ @XmlRootElement(name = "group") @XmlType(propOrder = { "name", "description", "admins", "members", "shared" }) +@Schema(description = "An Openfire user group.") public class GroupEntity { /** The name. */ @@ -75,7 +76,7 @@ public GroupEntity(String name, String description) { * @return the name */ @XmlElement - @Schema(description = "Name of the group", example = "UserGroup1") + @Schema(description = "The name of the group. When updating a group, this must be equal to the group name in the path of the request.", example = "UserGroup1", requiredMode = Schema.RequiredMode.REQUIRED) public String getName() { return name; } @@ -96,7 +97,7 @@ public void setName(String name) { * @return the description */ @XmlElement - @Schema(description = "Description of the group", example = "My group of users") + @Schema(description = "The description of the group.", example = "My group of users") public String getDescription() { return description; } @@ -119,7 +120,7 @@ public void setDescription(String description) { @XmlElementWrapper(name = "admins") @XmlElement(name = "admin") @JsonProperty(value = "admins") - @ArraySchema(schema = @Schema(example = "jane.smith"), arraySchema = @Schema(description = "List of admins of the group")) + @ArraySchema(schema = @Schema(example = "jane.smith"), arraySchema = @Schema(description = "The admins of the group. When creating or updating a group, each admin can be identified by a username or a JID. Responses contain (bare) JIDs.")) public List<String> getAdmins() { return admins; } @@ -132,7 +133,7 @@ public List<String> getAdmins() { @XmlElementWrapper(name = "members") @XmlElement(name = "member") @JsonProperty(value = "members") - @ArraySchema(schema = @Schema(example = "john.jones"), arraySchema = @Schema(description = "List of members of the group")) + @ArraySchema(schema = @Schema(example = "john.jones"), arraySchema = @Schema(description = "The members of the group. When creating or updating a group, each member can be identified by a username or a JID. Responses contain (bare) JIDs.")) public List<String> getMembers() { return members; } @@ -162,7 +163,7 @@ public void setMembers(List<String> members) { * @return whether it's a shared group */ @XmlElement(name = "shared") - @Schema(description = "Whether the group should automatically appear in the rosters of the users", example = "false") + @Schema(description = "Whether the group is shared: whether it automatically appears in the rosters of its members.", example = "false") public Boolean getShared(){ return shared; } /** diff --git a/src/java/org/jivesoftware/openfire/plugin/rest/entity/MUCInvitationEntity.java b/src/java/org/jivesoftware/openfire/plugin/rest/entity/MUCInvitationEntity.java index 907daa31c..8dde69535 100644 --- a/src/java/org/jivesoftware/openfire/plugin/rest/entity/MUCInvitationEntity.java +++ b/src/java/org/jivesoftware/openfire/plugin/rest/entity/MUCInvitationEntity.java @@ -16,12 +16,14 @@ package org.jivesoftware.openfire.plugin.rest.entity; + import io.swagger.v3.oas.annotations.media.Schema; import javax.xml.bind.annotation.XmlElement; import javax.xml.bind.annotation.XmlRootElement; @XmlRootElement(name = "mucInvitation") +@Schema(description = "An invitation to join a multi-user chat room.") public class MUCInvitationEntity { String reason; @@ -30,7 +32,7 @@ public MUCInvitationEntity() { } @XmlElement - @Schema(description = "The reason that will be included in the invitation message(s)", example = "Come join this cool room please!") + @Schema(description = "The reason that is included in the invitation message(s).", example = "Come join this cool room please!") public String getReason() { return reason; } diff --git a/src/java/org/jivesoftware/openfire/plugin/rest/entity/MUCInvitationsEntity.java b/src/java/org/jivesoftware/openfire/plugin/rest/entity/MUCInvitationsEntity.java index d0d3a7b9e..a92e6449b 100644 --- a/src/java/org/jivesoftware/openfire/plugin/rest/entity/MUCInvitationsEntity.java +++ b/src/java/org/jivesoftware/openfire/plugin/rest/entity/MUCInvitationsEntity.java @@ -17,6 +17,7 @@ package org.jivesoftware.openfire.plugin.rest.entity; import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.ArraySchema; import io.swagger.v3.oas.annotations.media.Schema; import javax.xml.bind.annotation.XmlElement; @@ -26,6 +27,7 @@ import java.util.List; @XmlRootElement(name = "mucInvitations") +@Schema(description = "An invitation for a collection of users and/or groups to join a multi-user chat room.") public class MUCInvitationsEntity extends MUCInvitationEntity { public MUCInvitationsEntity() { @@ -37,7 +39,7 @@ public MUCInvitationsEntity() { @XmlElementWrapper(name = "jidsToInvite") @XmlElement(name = "jid") @JsonProperty(value = "jidsToInvite") - @Schema(description = "The JIDs and/or names of the users and groups to invite into the room") + @ArraySchema(arraySchema = @Schema(description = "The users and/or groups to invite into the room, each identified by the JID of a user or group, or by the name of a local user or group."), schema = @Schema(example = "john@example.org")) public List<String> getJidsToInvite() { if (jidsToInvite == null) { jidsToInvite = new ArrayList<>(); diff --git a/src/java/org/jivesoftware/openfire/plugin/rest/entity/MUCRoomEntities.java b/src/java/org/jivesoftware/openfire/plugin/rest/entity/MUCRoomEntities.java index c3d00ed62..01ffa16a6 100644 --- a/src/java/org/jivesoftware/openfire/plugin/rest/entity/MUCRoomEntities.java +++ b/src/java/org/jivesoftware/openfire/plugin/rest/entity/MUCRoomEntities.java @@ -17,6 +17,8 @@ package org.jivesoftware.openfire.plugin.rest.entity; import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.ArraySchema; +import io.swagger.v3.oas.annotations.media.Schema; import java.util.List; @@ -24,6 +26,7 @@ import javax.xml.bind.annotation.XmlRootElement; @XmlRootElement(name = "chatRooms") +@Schema(description = "A list of multi-user chat rooms.") public class MUCRoomEntities { List<MUCRoomEntity> mucRooms; @@ -36,6 +39,7 @@ public MUCRoomEntities(List<MUCRoomEntity> mucRooms) { @XmlElement(name = "chatRoom") @JsonProperty(value = "chatRooms") + @ArraySchema(arraySchema = @Schema(description = "The chat rooms.")) public List<MUCRoomEntity> getMucRooms() { return mucRooms; } diff --git a/src/java/org/jivesoftware/openfire/plugin/rest/entity/MUCRoomEntity.java b/src/java/org/jivesoftware/openfire/plugin/rest/entity/MUCRoomEntity.java index e846ac042..40cf8e1ff 100644 --- a/src/java/org/jivesoftware/openfire/plugin/rest/entity/MUCRoomEntity.java +++ b/src/java/org/jivesoftware/openfire/plugin/rest/entity/MUCRoomEntity.java @@ -17,6 +17,7 @@ package org.jivesoftware.openfire.plugin.rest.entity; import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.ArraySchema; import io.swagger.v3.oas.annotations.media.Schema; import org.xmpp.packet.JID; @@ -34,6 +35,7 @@ "canOccupantsChangeSubject", "canOccupantsInvite", "canChangeNickname", "logEnabled", "loginRestrictedToNickname", "membersOnly", "moderated", "broadcastPresenceRoles", "owners", "admins", "members", "outcasts", "ownerGroups", "adminGroups", "memberGroups", "outcastGroups", "allowPM" }) +@Schema(description = "A multi-user chat room. When a room is created or updated, boolean values that are not provided are treated as 'false'.") public class MUCRoomEntity { private String roomName; @@ -84,6 +86,7 @@ public MUCRoomEntity(String naturalName, String roomName, String description) { } @XmlElement + @Schema(description = "The human-readable name of the room, as shown to users that discover rooms on the chat service.", example = "Global Chat") public String getNaturalName() { return naturalName; } @@ -93,6 +96,7 @@ public void setNaturalName(String naturalName) { } @XmlElement + @Schema(description = "The name of the room, which is used as the local part of the room's JID. It is converted to lowercase. When updating a room, this must be equal to the room name in the path of the request.", example = "global", requiredMode = Schema.RequiredMode.REQUIRED) public String getRoomName() { return roomName; } @@ -102,6 +106,7 @@ public void setRoomName(String roomName) { } @XmlElement + @Schema(description = "The description of the room.", example = "A room for everyone") public String getDescription() { return description; } @@ -111,6 +116,7 @@ public void setDescription(String description) { } @XmlElement + @Schema(description = "The password that users must provide to enter the room.", example = "s3cr3t") public String getPassword() { return password; } @@ -120,6 +126,7 @@ public void setPassword(String password) { } @XmlElement + @Schema(description = "The subject (topic) of the room.", example = "Welcome!") public String getSubject() { return subject; } @@ -129,6 +136,7 @@ public void setSubject(String subject) { } @XmlElement + @Schema(description = "The maximum number of occupants that can be in the room at the same time. 0 means unlimited.", example = "30") public int getMaxUsers() { return maxUsers; } @@ -138,6 +146,7 @@ public void setMaxUsers(int maxUsers) { } @XmlElement + @Schema(description = "The moment at which the room was created. When creating a room without this value, the current time is used.") public Date getCreationDate() { return creationDate; } @@ -147,6 +156,7 @@ public void setCreationDate(Date creationDate) { } @XmlElement + @Schema(description = "The moment at which the configuration of the room was last modified. When creating or updating a room without this value, the current time is used.") public Date getModificationDate() { return modificationDate; } @@ -156,6 +166,7 @@ public void setModificationDate(Date modificationDate) { } @XmlElement + @Schema(description = "Whether the room is persistent. Persistent rooms are saved to the database, and are not destroyed when the last occupant leaves.", example = "true") public boolean isPersistent() { return persistent; } @@ -165,6 +176,7 @@ public void setPersistent(boolean persistent) { } @XmlElement + @Schema(description = "Whether the room is public: searchable and visible through service discovery.", example = "true") public boolean isPublicRoom() { return publicRoom; } @@ -174,6 +186,7 @@ public void setPublicRoom(boolean publicRoom) { } @XmlElement + @Schema(description = "Whether users are allowed to register with the room.", example = "false") public boolean isRegistrationEnabled() { return registrationEnabled; } @@ -183,6 +196,7 @@ public void setRegistrationEnabled(boolean registrationEnabled) { } @XmlElement + @Schema(description = "Whether the real JID of every occupant is visible to every other occupant (a non-anonymous room).", example = "false") public boolean isCanAnyoneDiscoverJID() { return canAnyoneDiscoverJID; } @@ -192,6 +206,7 @@ public void setCanAnyoneDiscoverJID(boolean canAnyoneDiscoverJID) { } @XmlElement + @Schema(description = "Whether participants are allowed to change the subject of the room.", example = "false") public boolean isCanOccupantsChangeSubject() { return canOccupantsChangeSubject; } @@ -201,6 +216,7 @@ public void setCanOccupantsChangeSubject(boolean canOccupantsChangeSubject) { } @XmlElement + @Schema(description = "Whether occupants can invite other users to the room. When the room is not members-only, anyone can send invitations regardless of this value. When the room is members-only and this is 'false', only owners and admins can send invitations.", example = "false") public boolean isCanOccupantsInvite() { return canOccupantsInvite; } @@ -214,6 +230,7 @@ public void setBroadcastPresenceRoles(List<String> broadcastPresenceRoles) { } @XmlElement + @Schema(description = "Whether occupants are allowed to change their nickname in the room.", example = "true") public boolean isCanChangeNickname() { return canChangeNickname; } @@ -223,6 +240,7 @@ public void setCanChangeNickname(boolean canChangeNickname) { } @XmlElement + @Schema(description = "Whether the conversation in the room is logged (saved to the database).", example = "true") public boolean isLogEnabled() { return logEnabled; } @@ -232,6 +250,7 @@ public void setLogEnabled(boolean logEnabled) { } @XmlElement + @Schema(description = "Whether registered users can only join the room using their registered nickname.", example = "false") public boolean isLoginRestrictedToNickname() { return loginRestrictedToNickname; } @@ -241,6 +260,7 @@ public void setLoginRestrictedToNickname(boolean loginRestrictedToNickname) { } @XmlElement + @Schema(description = "Whether the room is members-only: users need to be a member (or be invited) to enter.", example = "false") public boolean isMembersOnly() { return membersOnly; } @@ -250,6 +270,7 @@ public void setMembersOnly(boolean membersOnly) { } @XmlElement + @Schema(description = "Whether the room is moderated: only occupants with 'voice' can send messages to all occupants.", example = "false") public boolean isModerated() { return moderated; } @@ -271,6 +292,7 @@ public void setAllowPM(String allowPM) { @XmlElement(name = "broadcastPresenceRole") @XmlElementWrapper(name = "broadcastPresenceRoles") @JsonProperty(value = "broadcastPresenceRoles") + @ArraySchema(arraySchema = @Schema(description = "The roles of occupants of which presence is broadcast to the other occupants. Each is one of: 'moderator', 'participant', 'visitor'."), schema = @Schema(example = "moderator")) public List<String> getBroadcastPresenceRoles() { return broadcastPresenceRoles; } @@ -278,6 +300,7 @@ public List<String> getBroadcastPresenceRoles() { @XmlElementWrapper(name = "owners") @XmlElement(name = "owner") @JsonProperty(value = "owners") + @ArraySchema(arraySchema = @Schema(description = "The (bare) JIDs of the users that have an owner affiliation with the room. When creating a room without owners, the 'admin' user is made owner."), schema = @Schema(example = "admin@example.org")) public List<String> getOwners() { return owners; } @@ -285,6 +308,7 @@ public List<String> getOwners() { @XmlElementWrapper(name = "ownerGroups") @XmlElement(name = "ownerGroup") @JsonProperty(value = "ownerGroups") + @ArraySchema(arraySchema = @Schema(description = "The names of the user groups that have an owner affiliation with the room."), schema = @Schema(example = "Management")) public List<String> getOwnerGroups() { return ownerGroups; } @@ -300,6 +324,7 @@ public void setOwnerGroups(List<String> ownerGroups) { @XmlElementWrapper(name = "members") @XmlElement(name = "member") @JsonProperty(value = "members") + @ArraySchema(arraySchema = @Schema(description = "The (bare) JIDs of the users that have a member affiliation with the room."), schema = @Schema(example = "john@example.org")) public List<String> getMembers() { return members; } @@ -307,6 +332,7 @@ public List<String> getMembers() { @XmlElementWrapper(name = "memberGroups") @XmlElement(name = "memberGroup") @JsonProperty(value = "memberGroups") + @ArraySchema(arraySchema = @Schema(description = "The names of the user groups that have a member affiliation with the room."), schema = @Schema(example = "Sales")) public List<String> getMemberGroups() { return memberGroups; } @@ -322,6 +348,7 @@ public void setMemberGroups(List<String> memberGroups) { @XmlElementWrapper(name = "outcasts") @XmlElement(name = "outcast") @JsonProperty(value = "outcasts") + @ArraySchema(arraySchema = @Schema(description = "The (bare) JIDs of the users that have an outcast affiliation with the room: users that are banned from the room."), schema = @Schema(example = "spammer@example.org")) public List<String> getOutcasts() { return outcasts; } @@ -329,6 +356,7 @@ public List<String> getOutcasts() { @XmlElementWrapper(name = "outcastGroups") @XmlElement(name = "outcastGroup") @JsonProperty(value = "outcastGroups") + @ArraySchema(arraySchema = @Schema(description = "The names of the user groups that have an outcast affiliation with the room."), schema = @Schema(example = "Banned")) public List<String> getOutcastGroups() { return outcastGroups; } @@ -344,6 +372,7 @@ public void setOutcastGroups(List<String> outcastGroups) { @XmlElementWrapper(name = "admins") @XmlElement(name = "admin") @JsonProperty(value = "admins") + @ArraySchema(arraySchema = @Schema(description = "The (bare) JIDs of the users that have an admin affiliation with the room."), schema = @Schema(example = "jane@example.org")) public List<String> getAdmins() { return admins; } @@ -351,6 +380,7 @@ public List<String> getAdmins() { @XmlElementWrapper(name = "adminGroups") @XmlElement(name = "adminGroup") @JsonProperty(value = "adminGroups") + @ArraySchema(arraySchema = @Schema(description = "The names of the user groups that have an admin affiliation with the room."), schema = @Schema(example = "Moderators")) public List<String> getAdminGroups() { return adminGroups; } diff --git a/src/java/org/jivesoftware/openfire/plugin/rest/entity/MUCRoomMessageEntities.java b/src/java/org/jivesoftware/openfire/plugin/rest/entity/MUCRoomMessageEntities.java index abbbaaf4d..e1269dea3 100644 --- a/src/java/org/jivesoftware/openfire/plugin/rest/entity/MUCRoomMessageEntities.java +++ b/src/java/org/jivesoftware/openfire/plugin/rest/entity/MUCRoomMessageEntities.java @@ -16,11 +16,16 @@ package org.jivesoftware.openfire.plugin.rest.entity; +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.ArraySchema; +import io.swagger.v3.oas.annotations.media.Schema; + import javax.xml.bind.annotation.XmlElement; import javax.xml.bind.annotation.XmlRootElement; import java.util.List; @XmlRootElement(name = "messages") +@Schema(description = "A list of messages from the history of a multi-user chat room.") public class MUCRoomMessageEntities { List<MUCRoomMessageEntity> messages; @@ -32,6 +37,8 @@ public MUCRoomMessageEntities(List<MUCRoomMessageEntity> messages) { } @XmlElement(name = "message") + @JsonProperty(value = "message") + @ArraySchema(arraySchema = @Schema(description = "The messages.")) public List<MUCRoomMessageEntity> getMessages() { return messages; } diff --git a/src/java/org/jivesoftware/openfire/plugin/rest/entity/MUCRoomMessageEntity.java b/src/java/org/jivesoftware/openfire/plugin/rest/entity/MUCRoomMessageEntity.java index d108381c0..49404d354 100644 --- a/src/java/org/jivesoftware/openfire/plugin/rest/entity/MUCRoomMessageEntity.java +++ b/src/java/org/jivesoftware/openfire/plugin/rest/entity/MUCRoomMessageEntity.java @@ -16,6 +16,9 @@ package org.jivesoftware.openfire.plugin.rest.entity; +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.Schema; + import javax.xml.bind.annotation.XmlElement; import javax.xml.bind.annotation.XmlRootElement; import javax.xml.bind.annotation.XmlType; @@ -24,6 +27,7 @@ //xmlns="jabber:x:event"><composing/></x></message> @XmlRootElement(name = "message") @XmlType(propOrder = { "to", "from", "type", "body", "delayStamp", "delayFrom"}) +@Schema(description = "A message from the history of a multi-user chat room.") public class MUCRoomMessageEntity { String to; String from; @@ -33,6 +37,7 @@ public class MUCRoomMessageEntity { String delayFrom; @XmlElement + @Schema(description = "The JID of the addressee of the message.", example = "global@conference.example.org") public String getTo() { return to; } @@ -41,6 +46,7 @@ public void setTo(String to) { } @XmlElement + @Schema(description = "The JID of the sender of the message: the room JID, followed by the nickname of the occupant.", example = "global@conference.example.org/john") public String getFrom() { return from; } @@ -49,6 +55,7 @@ public void setFrom(String from) { } @XmlElement + @Schema(description = "The XMPP message type.", example = "groupchat") public String getType() { return type; } @@ -57,12 +64,15 @@ public void setType(String type) { } @XmlElement(name="delay_stamp") + @JsonProperty(value = "delay_stamp") + @Schema(description = "The moment at which the message was originally sent (XEP-0203 delayed delivery timestamp).", example = "2026-01-31T12:34:56.789Z") public String getDelayStamp() { return delayStamp; } public void setDelayStamp(String delayStamp) { this.delayStamp = delayStamp; } @XmlElement + @Schema(description = "The text of the message.", example = "Hello, everyone!") public String getBody() { return body; } @@ -71,6 +81,8 @@ public void setBody(String body) { } @XmlElement(name="delay_from") + @JsonProperty(value = "delay_from") + @Schema(description = "The JID of the entity that delayed the delivery of the message (XEP-0203).", example = "global@conference.example.org") public String getDelayFrom() { return delayFrom; } public void setDelayFrom(String delayFrom) { this.delayFrom = delayFrom; } diff --git a/src/java/org/jivesoftware/openfire/plugin/rest/entity/MUCServiceEntities.java b/src/java/org/jivesoftware/openfire/plugin/rest/entity/MUCServiceEntities.java index 4accdf871..250dab04e 100644 --- a/src/java/org/jivesoftware/openfire/plugin/rest/entity/MUCServiceEntities.java +++ b/src/java/org/jivesoftware/openfire/plugin/rest/entity/MUCServiceEntities.java @@ -16,11 +16,16 @@ package org.jivesoftware.openfire.plugin.rest.entity; +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.ArraySchema; +import io.swagger.v3.oas.annotations.media.Schema; + import javax.xml.bind.annotation.XmlElement; import javax.xml.bind.annotation.XmlRootElement; import java.util.List; @XmlRootElement(name = "chatServices") +@Schema(description = "A list of multi-user chat services.") public class MUCServiceEntities { List<MUCServiceEntity> services; @@ -32,6 +37,8 @@ public MUCServiceEntities(List<MUCServiceEntity> services) { } @XmlElement(name = "chatService") + @JsonProperty(value = "chatService") + @ArraySchema(arraySchema = @Schema(description = "The chat services.")) public List<MUCServiceEntity> getServices() { return services; } diff --git a/src/java/org/jivesoftware/openfire/plugin/rest/entity/MUCServiceEntity.java b/src/java/org/jivesoftware/openfire/plugin/rest/entity/MUCServiceEntity.java index e52d008fb..8e4535aeb 100644 --- a/src/java/org/jivesoftware/openfire/plugin/rest/entity/MUCServiceEntity.java +++ b/src/java/org/jivesoftware/openfire/plugin/rest/entity/MUCServiceEntity.java @@ -16,6 +16,7 @@ package org.jivesoftware.openfire.plugin.rest.entity; + import io.swagger.v3.oas.annotations.media.Schema; import javax.xml.bind.annotation.XmlElement; @@ -24,6 +25,7 @@ @XmlRootElement(name = "chatService") @XmlType(propOrder = { "serviceName", "description", "hidden" }) +@Schema(description = "A multi-user chat service.") public class MUCServiceEntity { private String serviceName; @@ -44,7 +46,7 @@ public MUCServiceEntity(String serviceName, String description, boolean hidden) } @XmlElement - @Schema(description = "The name of the chat service", example = "conference") + @Schema(description = "The name of the chat service, which is used as the subdomain of the service.", example = "conference", requiredMode = Schema.RequiredMode.REQUIRED) public String getServiceName() { return serviceName; } @@ -54,7 +56,7 @@ public void setServiceName(String serviceName) { } @XmlElement - @Schema(description = "The description of the chat service", example = "A public service") + @Schema(description = "The description of the chat service.", example = "A public service") public String getDescription() { return description; } @@ -64,7 +66,7 @@ public void setDescription(String description) { } @XmlElement - @Schema(description = "Whether the service is hidden", example = "false") + @Schema(description = "Whether the service is hidden from service discovery.", example = "false") public boolean isHidden() { return hidden; } diff --git a/src/java/org/jivesoftware/openfire/plugin/rest/entity/MemberEntities.java b/src/java/org/jivesoftware/openfire/plugin/rest/entity/MemberEntities.java index 3a4b0f950..320141c97 100644 --- a/src/java/org/jivesoftware/openfire/plugin/rest/entity/MemberEntities.java +++ b/src/java/org/jivesoftware/openfire/plugin/rest/entity/MemberEntities.java @@ -17,12 +17,15 @@ package org.jivesoftware.openfire.plugin.rest.entity; import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.ArraySchema; +import io.swagger.v3.oas.annotations.media.Schema; import javax.xml.bind.annotation.XmlElement; import javax.xml.bind.annotation.XmlRootElement; import java.util.List; @XmlRootElement(name = "members") +@Schema(description = "A list of entities that have a member affiliation with a multi-user chat room.") public class MemberEntities extends AffiliatedEntities { List<String> members; @@ -36,6 +39,7 @@ public MemberEntities(List<String> members) { @XmlElement(name = "member") @JsonProperty(value = "members") + @ArraySchema(arraySchema = @Schema(description = "The JIDs (or names of local users) of the entities."), schema = @Schema(example = "john@example.org")) public List<String> getMembers() { return members; } diff --git a/src/java/org/jivesoftware/openfire/plugin/rest/entity/MessageEntity.java b/src/java/org/jivesoftware/openfire/plugin/rest/entity/MessageEntity.java index 3458cc1af..98aac948f 100644 --- a/src/java/org/jivesoftware/openfire/plugin/rest/entity/MessageEntity.java +++ b/src/java/org/jivesoftware/openfire/plugin/rest/entity/MessageEntity.java @@ -16,6 +16,8 @@ package org.jivesoftware.openfire.plugin.rest.entity; +import io.swagger.v3.oas.annotations.media.Schema; + import javax.xml.bind.annotation.XmlElement; import javax.xml.bind.annotation.XmlRootElement; @@ -23,6 +25,7 @@ * The Class MessageEntity. */ @XmlRootElement(name = "message") +@Schema(description = "A message.") public class MessageEntity { /** The body. */ @@ -40,6 +43,7 @@ public MessageEntity() { * @return the body */ @XmlElement + @Schema(description = "The text of the message.", example = "The server will be restarted in 5 minutes.", requiredMode = Schema.RequiredMode.REQUIRED) public String getBody() { return body; } diff --git a/src/java/org/jivesoftware/openfire/plugin/rest/entity/MsgArchiveEntity.java b/src/java/org/jivesoftware/openfire/plugin/rest/entity/MsgArchiveEntity.java index f9d98b9f1..8f3b7999c 100644 --- a/src/java/org/jivesoftware/openfire/plugin/rest/entity/MsgArchiveEntity.java +++ b/src/java/org/jivesoftware/openfire/plugin/rest/entity/MsgArchiveEntity.java @@ -16,22 +16,25 @@ package org.jivesoftware.openfire.plugin.rest.entity; +import io.swagger.v3.oas.annotations.media.Schema; + import javax.xml.bind.annotation.XmlElement; import javax.xml.bind.annotation.XmlRootElement; +import javax.xml.bind.annotation.XmlType; /** * The Class MsgArchiveEntity. */ @XmlRootElement(name = "archive") +@XmlType(propOrder = { "jid", "count" }) +@Schema(description = "The number of unread messages of a user.") public class MsgArchiveEntity { - @XmlElement String jid; /** * unread messages count */ - @XmlElement int count; public MsgArchiveEntity() { @@ -42,4 +45,15 @@ public MsgArchiveEntity(String jid, int count) { this.count = count; } + @XmlElement + @Schema(description = "The JID of the user.", example = "john@example.org") + public String getJid() { + return jid; + } + + @XmlElement + @Schema(description = "The number of unread messages.", example = "3") + public int getCount() { + return count; + } } diff --git a/src/java/org/jivesoftware/openfire/plugin/rest/entity/OccupantEntities.java b/src/java/org/jivesoftware/openfire/plugin/rest/entity/OccupantEntities.java index 3d4b9b978..c61cd16bf 100644 --- a/src/java/org/jivesoftware/openfire/plugin/rest/entity/OccupantEntities.java +++ b/src/java/org/jivesoftware/openfire/plugin/rest/entity/OccupantEntities.java @@ -17,12 +17,15 @@ package org.jivesoftware.openfire.plugin.rest.entity; import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.ArraySchema; +import io.swagger.v3.oas.annotations.media.Schema; import javax.xml.bind.annotation.XmlElement; import javax.xml.bind.annotation.XmlRootElement; import java.util.List; @XmlRootElement(name = "occupants") +@Schema(description = "A list of occupants of a multi-user chat room.") public class OccupantEntities { List<OccupantEntity> occupants; @@ -35,6 +38,7 @@ public OccupantEntities(List<OccupantEntity> occupants) { @XmlElement(name = "occupant") @JsonProperty(value = "occupants") + @ArraySchema(arraySchema = @Schema(description = "The occupants.")) public List<OccupantEntity> getOccupants() { return occupants; } diff --git a/src/java/org/jivesoftware/openfire/plugin/rest/entity/OccupantEntity.java b/src/java/org/jivesoftware/openfire/plugin/rest/entity/OccupantEntity.java index e546c74f1..d302ef31d 100644 --- a/src/java/org/jivesoftware/openfire/plugin/rest/entity/OccupantEntity.java +++ b/src/java/org/jivesoftware/openfire/plugin/rest/entity/OccupantEntity.java @@ -16,10 +16,13 @@ package org.jivesoftware.openfire.plugin.rest.entity; +import io.swagger.v3.oas.annotations.media.Schema; + import javax.xml.bind.annotation.XmlElement; import javax.xml.bind.annotation.XmlRootElement; @XmlRootElement(name = "occupant") +@Schema(description = "An occupant of a multi-user chat room.") public class OccupantEntity { private String jid; @@ -31,6 +34,7 @@ public OccupantEntity() { } @XmlElement + @Schema(description = "The occupant JID: the room JID, followed by the nickname of the occupant.", example = "global@conference.example.org/john") public String getJid() { return jid; } @@ -40,6 +44,7 @@ public void setJid(String jid) { } @XmlElement + @Schema(description = "The role of the occupant in the room. One of: 'moderator', 'participant', 'visitor', 'none'.", example = "participant") public String getRole() { return role; } @@ -49,6 +54,7 @@ public void setRole(String role) { } @XmlElement + @Schema(description = "The affiliation of the occupant with the room. One of: 'owner', 'admin', 'member', 'outcast', 'none'.", example = "member") public String getAffiliation() { return affiliation; } @@ -58,6 +64,7 @@ public void setAffiliation(String affiliation) { } @XmlElement + @Schema(description = "The real (full) JID of the user.", example = "john@example.org/laptop") public String getUserAddress() { return userAddress; } diff --git a/src/java/org/jivesoftware/openfire/plugin/rest/entity/OutcastEntities.java b/src/java/org/jivesoftware/openfire/plugin/rest/entity/OutcastEntities.java index b22e06332..0b965683b 100644 --- a/src/java/org/jivesoftware/openfire/plugin/rest/entity/OutcastEntities.java +++ b/src/java/org/jivesoftware/openfire/plugin/rest/entity/OutcastEntities.java @@ -17,12 +17,15 @@ package org.jivesoftware.openfire.plugin.rest.entity; import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.ArraySchema; +import io.swagger.v3.oas.annotations.media.Schema; import javax.xml.bind.annotation.XmlElement; import javax.xml.bind.annotation.XmlRootElement; import java.util.List; @XmlRootElement(name = "outcasts") +@Schema(description = "A list of entities that have an outcast affiliation with a multi-user chat room: entities that are banned from the room.") public class OutcastEntities extends AffiliatedEntities { List<String> outcasts; @@ -36,6 +39,7 @@ public OutcastEntities(List<String> outcasts) { @XmlElement(name = "outcast") @JsonProperty(value = "outcasts") + @ArraySchema(arraySchema = @Schema(description = "The JIDs (or names of local users) of the entities."), schema = @Schema(example = "spammer@example.org")) public List<String> getOutcasts() { return outcasts; } diff --git a/src/java/org/jivesoftware/openfire/plugin/rest/entity/OwnerEntities.java b/src/java/org/jivesoftware/openfire/plugin/rest/entity/OwnerEntities.java index 5dc72f467..ca3cd2bad 100644 --- a/src/java/org/jivesoftware/openfire/plugin/rest/entity/OwnerEntities.java +++ b/src/java/org/jivesoftware/openfire/plugin/rest/entity/OwnerEntities.java @@ -17,12 +17,15 @@ package org.jivesoftware.openfire.plugin.rest.entity; import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.ArraySchema; +import io.swagger.v3.oas.annotations.media.Schema; import javax.xml.bind.annotation.XmlElement; import javax.xml.bind.annotation.XmlRootElement; import java.util.List; @XmlRootElement(name = "owners") +@Schema(description = "A list of entities that have an owner affiliation with a multi-user chat room.") public class OwnerEntities extends AffiliatedEntities { List<String> owners; @@ -36,6 +39,7 @@ public OwnerEntities(List<String> owners) { @XmlElement(name = "owner") @JsonProperty(value = "owners") + @ArraySchema(arraySchema = @Schema(description = "The JIDs (or names of local users) of the entities."), schema = @Schema(example = "admin@example.org")) public List<String> getOwners() { return owners; } diff --git a/src/java/org/jivesoftware/openfire/plugin/rest/entity/ParticipantEntities.java b/src/java/org/jivesoftware/openfire/plugin/rest/entity/ParticipantEntities.java index 6003a4df3..1255d7fb6 100644 --- a/src/java/org/jivesoftware/openfire/plugin/rest/entity/ParticipantEntities.java +++ b/src/java/org/jivesoftware/openfire/plugin/rest/entity/ParticipantEntities.java @@ -17,6 +17,8 @@ package org.jivesoftware.openfire.plugin.rest.entity; import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.ArraySchema; +import io.swagger.v3.oas.annotations.media.Schema; import java.util.List; @@ -24,6 +26,7 @@ import javax.xml.bind.annotation.XmlRootElement; @XmlRootElement(name = "participants") +@Schema(description = "A list of occupants of a multi-user chat room.") public class ParticipantEntities { List<ParticipantEntity> participants; @@ -36,6 +39,7 @@ public ParticipantEntities(List<ParticipantEntity> participants) { @XmlElement(name = "participant") @JsonProperty(value = "participants") + @ArraySchema(arraySchema = @Schema(description = "The occupants.")) public List<ParticipantEntity> getParticipants() { return participants; } diff --git a/src/java/org/jivesoftware/openfire/plugin/rest/entity/ParticipantEntity.java b/src/java/org/jivesoftware/openfire/plugin/rest/entity/ParticipantEntity.java index cdcd29751..6fda36a9b 100644 --- a/src/java/org/jivesoftware/openfire/plugin/rest/entity/ParticipantEntity.java +++ b/src/java/org/jivesoftware/openfire/plugin/rest/entity/ParticipantEntity.java @@ -16,10 +16,13 @@ package org.jivesoftware.openfire.plugin.rest.entity; +import io.swagger.v3.oas.annotations.media.Schema; + import javax.xml.bind.annotation.XmlElement; import javax.xml.bind.annotation.XmlRootElement; @XmlRootElement(name = "participant") +@Schema(description = "An occupant of a multi-user chat room.") public class ParticipantEntity { private String jid; @@ -30,6 +33,7 @@ public ParticipantEntity() { } @XmlElement + @Schema(description = "The occupant JID: the room JID, followed by the nickname of the occupant.", example = "global@conference.example.org/john") public String getJid() { return jid; } @@ -39,6 +43,7 @@ public void setJid(String jid) { } @XmlElement + @Schema(description = "The role of the occupant in the room. One of: 'moderator', 'participant', 'visitor', 'none'.", example = "participant") public String getRole() { return role; } @@ -48,6 +53,7 @@ public void setRole(String role) { } @XmlElement + @Schema(description = "The affiliation of the occupant with the room. One of: 'owner', 'admin', 'member', 'outcast', 'none'.", example = "member") public String getAffiliation() { return affiliation; } diff --git a/src/java/org/jivesoftware/openfire/plugin/rest/entity/RoomCreationResultEntities.java b/src/java/org/jivesoftware/openfire/plugin/rest/entity/RoomCreationResultEntities.java index db18e0662..659711012 100644 --- a/src/java/org/jivesoftware/openfire/plugin/rest/entity/RoomCreationResultEntities.java +++ b/src/java/org/jivesoftware/openfire/plugin/rest/entity/RoomCreationResultEntities.java @@ -17,6 +17,7 @@ package org.jivesoftware.openfire.plugin.rest.entity; import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.ArraySchema; import io.swagger.v3.oas.annotations.media.Schema; import javax.xml.bind.annotation.XmlElement; @@ -28,6 +29,7 @@ @XmlRootElement(name = "results") @XmlType(propOrder = { "successResults", "failureResults", "otherResults" }) +@Schema(description = "The results of the creation of multiple multi-user chat rooms, grouped by result type.") public class RoomCreationResultEntities { List<RoomCreationResultEntity> successResults; List<RoomCreationResultEntity> failureResults; @@ -66,7 +68,7 @@ public void addResult(RoomCreationResultEntity resultToAdd) { @XmlElement(name = "result") @XmlElementWrapper(name = "success") @JsonProperty(value = "success") - @Schema(description = "All creation results of type success") + @ArraySchema(arraySchema = @Schema(description = "The results of the rooms that were created successfully.")) public List<RoomCreationResultEntity> getSuccessResults() { return successResults; } @@ -74,7 +76,7 @@ public List<RoomCreationResultEntity> getSuccessResults() { @XmlElement(name = "result") @XmlElementWrapper(name = "failure") @JsonProperty(value = "failure") - @Schema(description = "All creation results of type failure") + @ArraySchema(arraySchema = @Schema(description = "The results of the rooms that could not be created.")) public List<RoomCreationResultEntity> getFailureResults() { return failureResults; } @@ -82,7 +84,7 @@ public List<RoomCreationResultEntity> getFailureResults() { @XmlElement(name = "result") @XmlElementWrapper(name = "other") @JsonProperty(value = "other") - @Schema(description = "All creation results of a type other than success or failure") + @ArraySchema(arraySchema = @Schema(description = "The results of a type other than success or failure.")) public List<RoomCreationResultEntity> getOtherResults() { return otherResults; } diff --git a/src/java/org/jivesoftware/openfire/plugin/rest/entity/RoomCreationResultEntity.java b/src/java/org/jivesoftware/openfire/plugin/rest/entity/RoomCreationResultEntity.java index b421d0c03..1a4346b38 100644 --- a/src/java/org/jivesoftware/openfire/plugin/rest/entity/RoomCreationResultEntity.java +++ b/src/java/org/jivesoftware/openfire/plugin/rest/entity/RoomCreationResultEntity.java @@ -26,6 +26,7 @@ @XmlRootElement(name = "result") @XmlType(propOrder = { "roomName", "resultType", "message"}) +@Schema(description = "The result of the creation of one multi-user chat room.") public class RoomCreationResultEntity { public enum RoomCreationResultType { @@ -37,7 +38,7 @@ public enum RoomCreationResultType { String message; @XmlElement - @Schema(description = "The name of the room that was to be created", example = "open_chat") + @Schema(description = "The name of the room that was to be created.", example = "open_chat") public String getRoomName() { return roomName; } @@ -47,7 +48,7 @@ public void setRoomName(String roomName) { } @XmlElement - @Schema(description = "The result of creating the room", example = "Failure") + @Schema(description = "The result of creating the room.", example = "Failure") public RoomCreationResultType getResultType() { return resultType; } @@ -57,7 +58,7 @@ public void setResultType(RoomCreationResultType resultType) { } @XmlElement - @Schema(description = "A message describing the result", example = "Room already existed and therefore not created again") + @Schema(description = "A message that describes the result.", example = "Room already existed and therefore not created again") public String getMessage() { return message; } diff --git a/src/java/org/jivesoftware/openfire/plugin/rest/entity/RosterEntities.java b/src/java/org/jivesoftware/openfire/plugin/rest/entity/RosterEntities.java index 42f516d09..1f4292f1f 100644 --- a/src/java/org/jivesoftware/openfire/plugin/rest/entity/RosterEntities.java +++ b/src/java/org/jivesoftware/openfire/plugin/rest/entity/RosterEntities.java @@ -16,6 +16,10 @@ package org.jivesoftware.openfire.plugin.rest.entity; +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.ArraySchema; +import io.swagger.v3.oas.annotations.media.Schema; + import java.util.List; import javax.xml.bind.annotation.XmlElement; @@ -25,6 +29,7 @@ * The Class RosterEntities. */ @XmlRootElement(name = "roster") +@Schema(description = "The roster (contact list) of a user.") public class RosterEntities { /** The roster. */ @@ -53,6 +58,8 @@ public RosterEntities(List<RosterItemEntity> roster) { * @return the roster */ @XmlElement(name = "rosterItem") + @JsonProperty(value = "rosterItem") + @ArraySchema(arraySchema = @Schema(description = "The entries of the roster.")) public List<RosterItemEntity> getRoster() { return roster; } diff --git a/src/java/org/jivesoftware/openfire/plugin/rest/entity/RosterItemEntity.java b/src/java/org/jivesoftware/openfire/plugin/rest/entity/RosterItemEntity.java index a2606cfac..8d50f562c 100644 --- a/src/java/org/jivesoftware/openfire/plugin/rest/entity/RosterItemEntity.java +++ b/src/java/org/jivesoftware/openfire/plugin/rest/entity/RosterItemEntity.java @@ -17,6 +17,8 @@ package org.jivesoftware.openfire.plugin.rest.entity; import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.ArraySchema; +import io.swagger.v3.oas.annotations.media.Schema; import java.util.List; @@ -30,6 +32,7 @@ */ @XmlRootElement(name = "rosterItem") @XmlType(propOrder = { "jid", "nickname", "subscriptionType", "groups" }) +@Schema(description = "An entry in the roster (contact list) of a user.") public class RosterItemEntity { /** The jid. */ @@ -73,6 +76,7 @@ public RosterItemEntity(String jid, String nickname, int subscriptionType) { * @return the jid */ @XmlElement + @Schema(description = "The JID of the contact.", example = "jane@example.org", requiredMode = Schema.RequiredMode.REQUIRED) public String getJid() { return jid; } @@ -93,6 +97,7 @@ public void setJid(String jid) { * @return the nickname */ @XmlElement + @Schema(description = "The name of the contact, as shown in this roster.", example = "Jane") public String getNickname() { return nickname; } @@ -113,6 +118,7 @@ public void setNickname(String nickname) { * @return the subscription type */ @XmlElement + @Schema(description = "The presence subscription state of the contact. One of: -1 (remove), 0 (none), 1 (to: the user receives presence updates of the contact), 2 (from: the contact receives presence updates of the user), 3 (both).", example = "3") public int getSubscriptionType() { return subscriptionType; } @@ -135,6 +141,7 @@ public void setSubscriptionType(int subscriptionType) { @XmlElement(name = "group") @XmlElementWrapper(name = "groups") @JsonProperty(value = "groups") + @ArraySchema(arraySchema = @Schema(description = "The roster groups (for example 'Friends' or 'Co-workers') that this contact is organized under."), schema = @Schema(example = "Friends")) public List<String> getGroups() { return groups; } diff --git a/src/java/org/jivesoftware/openfire/plugin/rest/entity/SecurityAuditLog.java b/src/java/org/jivesoftware/openfire/plugin/rest/entity/SecurityAuditLog.java index 24bfc8b75..b6e696477 100644 --- a/src/java/org/jivesoftware/openfire/plugin/rest/entity/SecurityAuditLog.java +++ b/src/java/org/jivesoftware/openfire/plugin/rest/entity/SecurityAuditLog.java @@ -16,6 +16,8 @@ package org.jivesoftware.openfire.plugin.rest.entity; +import io.swagger.v3.oas.annotations.media.Schema; + import javax.xml.bind.annotation.XmlElement; import javax.xml.bind.annotation.XmlRootElement; @@ -23,6 +25,7 @@ * The Class SecurityAuditLog. */ @XmlRootElement(name = "log") +@Schema(description = "An entry of the security audit log.") public class SecurityAuditLog { /** The log id. */ @@ -74,6 +77,7 @@ public SecurityAuditLog(long logId, String username, long timestamp, String summ * @return the log id */ @XmlElement + @Schema(description = "The unique identifier of the log entry.", example = "42") public long getLogId() { return logId; } @@ -93,6 +97,7 @@ public void setLogId(long logId) { * @return the username */ @XmlElement + @Schema(description = "The username of the user that performed the audited action.", example = "admin") public String getUsername() { return username; } @@ -112,6 +117,7 @@ public void setUsername(String username) { * @return the timestamp */ @XmlElement + @Schema(description = "The moment at which the audited action occurred, in seconds since the Unix epoch.", example = "1769862896") public long getTimestamp() { return timestamp; } @@ -131,6 +137,7 @@ public void setTimestamp(long timestamp) { * @return the summary */ @XmlElement + @Schema(description = "A short description of the audited action.", example = "Created new user john") public String getSummary() { return summary; } @@ -150,6 +157,7 @@ public void setSummary(String summary) { * @return the node */ @XmlElement + @Schema(description = "The node that triggered the audited action, usually a host name or IP address.", example = "xmpp1.example.org") public String getNode() { return node; } @@ -169,6 +177,7 @@ public void setNode(String node) { * @return the details */ @XmlElement + @Schema(description = "Detailed information about the audited action.", example = "name = John Doe, email = john@example.org") public String getDetails() { return details; } diff --git a/src/java/org/jivesoftware/openfire/plugin/rest/entity/SecurityAuditLogs.java b/src/java/org/jivesoftware/openfire/plugin/rest/entity/SecurityAuditLogs.java index ac415f85f..23a0187e3 100644 --- a/src/java/org/jivesoftware/openfire/plugin/rest/entity/SecurityAuditLogs.java +++ b/src/java/org/jivesoftware/openfire/plugin/rest/entity/SecurityAuditLogs.java @@ -17,6 +17,8 @@ package org.jivesoftware.openfire.plugin.rest.entity; import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.ArraySchema; +import io.swagger.v3.oas.annotations.media.Schema; import java.util.List; @@ -24,6 +26,7 @@ import javax.xml.bind.annotation.XmlRootElement; @XmlRootElement(name = "logs") +@Schema(description = "A list of entries of the security audit log.") public class SecurityAuditLogs { List<SecurityAuditLog> securityAuditLog; @@ -36,6 +39,7 @@ public SecurityAuditLogs(List<SecurityAuditLog> securityAuditLog) { @XmlElement(name = "log") @JsonProperty(value = "logs") + @ArraySchema(arraySchema = @Schema(description = "The log entries.")) public List<SecurityAuditLog> getSecurityAuditLog() { return securityAuditLog; } diff --git a/src/java/org/jivesoftware/openfire/plugin/rest/entity/SessionEntities.java b/src/java/org/jivesoftware/openfire/plugin/rest/entity/SessionEntities.java index 1d6699523..6f8107adf 100644 --- a/src/java/org/jivesoftware/openfire/plugin/rest/entity/SessionEntities.java +++ b/src/java/org/jivesoftware/openfire/plugin/rest/entity/SessionEntities.java @@ -17,6 +17,8 @@ package org.jivesoftware.openfire.plugin.rest.entity; import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.ArraySchema; +import io.swagger.v3.oas.annotations.media.Schema; import java.util.List; @@ -24,6 +26,7 @@ import javax.xml.bind.annotation.XmlRootElement; @XmlRootElement(name = "sessions") +@Schema(description = "A list of client sessions.") public class SessionEntities { List<SessionEntity> sessions; @@ -36,6 +39,7 @@ public SessionEntities(List<SessionEntity> sessions) { @XmlElement(name = "session") @JsonProperty(value = "sessions") + @ArraySchema(arraySchema = @Schema(description = "The sessions.")) public List<SessionEntity> getSessions() { return sessions; } diff --git a/src/java/org/jivesoftware/openfire/plugin/rest/entity/SessionEntity.java b/src/java/org/jivesoftware/openfire/plugin/rest/entity/SessionEntity.java index e0497dc54..2db501b8a 100644 --- a/src/java/org/jivesoftware/openfire/plugin/rest/entity/SessionEntity.java +++ b/src/java/org/jivesoftware/openfire/plugin/rest/entity/SessionEntity.java @@ -16,6 +16,8 @@ package org.jivesoftware.openfire.plugin.rest.entity; +import io.swagger.v3.oas.annotations.media.Schema; + import java.util.Date; import javax.xml.bind.annotation.XmlElement; @@ -25,6 +27,7 @@ @XmlRootElement(name = "session") @XmlType(propOrder = { "sessionId", "username", "resource", "node", "sessionStatus", "presenceStatus", "presenceMessage", "priority", "hostAddress", "hostName", "creationDate", "lastActionDate", "secure" }) +@Schema(description = "A client session.") public class SessionEntity { private String sessionId; @@ -47,6 +50,7 @@ public SessionEntity() { } @XmlElement + @Schema(description = "The (full) JID of the session.", example = "john@example.org/laptop") public String getSessionId() { return sessionId; } @@ -56,6 +60,7 @@ public void setSessionId(String sessionId) { } @XmlElement + @Schema(description = "The username of the user of the session, or 'Anonymous' for anonymous sessions.", example = "john") public String getUsername() { return username; } @@ -65,6 +70,7 @@ public void setUsername(String username) { } @XmlElement + @Schema(description = "The resource part of the JID of the session.", example = "laptop") public String getResource() { return resource; } @@ -74,6 +80,7 @@ public void setResource(String resource) { } @XmlElement + @Schema(description = "Whether the session is connected to the cluster node that processes the request ('Local'), or to another cluster node ('Remote').", example = "Local") public String getNode() { return node; } @@ -83,6 +90,7 @@ public void setNode(String node) { } @XmlElement + @Schema(description = "The status of the session. One of: 'Closed', 'Connected', 'Authenticated', 'Unknown'.", example = "Authenticated") public String getSessionStatus() { return sessionStatus; } @@ -92,6 +100,7 @@ public void setSessionStatus(String sessionStatus) { } @XmlElement + @Schema(description = "The availability of the user of the session. One of: 'Online', 'Away', 'Available to Chat', 'Do Not Disturb', 'Extended Away', 'Unknown/Not Recognized'.", example = "Online") public String getPresenceStatus() { return presenceStatus; } @@ -100,6 +109,7 @@ public void setPresenceStatus(String presenceStatus) { this.presenceStatus = presenceStatus; } + @Schema(description = "The (optional) natural-language description of the availability of the user of the session.", example = "In a meeting") public String getPresenceMessage() { return presenceMessage; } @@ -109,6 +119,7 @@ public void setPresenceMessage(String presenceMessage) { } @XmlElement + @Schema(description = "The presence priority of the session, from -128 to 127.", example = "0") public int getPriority() { return priority; } @@ -118,6 +129,7 @@ public void setPriority(int priority) { } @XmlElement + @Schema(description = "The IP address of the client.", example = "192.168.0.20") public String getHostAddress() { return hostAddress; } @@ -127,6 +139,7 @@ public void setHostAddress(String hostAddress) { } @XmlElement + @Schema(description = "The host name of the client.", example = "laptop.example.org") public String getHostName() { return hostName; } @@ -136,6 +149,7 @@ public void setHostName(String hostName) { } @XmlElement + @Schema(description = "The moment at which the session was created.") public Date getCreationDate() { return creationDate; } @@ -145,6 +159,7 @@ public void setCreationDate(Date creationDate) { } @XmlElement + @Schema(description = "The moment at which the session last had activity.") public Date getLastActionDate() { return lastActionDate; } @@ -154,6 +169,7 @@ public void setLastActionDate(Date lastActionDate) { } @XmlElement + @Schema(description = "Whether the connection of the session is encrypted.", example = "true") public boolean isSecure() { return secure; } diff --git a/src/java/org/jivesoftware/openfire/plugin/rest/entity/SessionsCount.java b/src/java/org/jivesoftware/openfire/plugin/rest/entity/SessionsCount.java index ee8f489bf..0619a535a 100644 --- a/src/java/org/jivesoftware/openfire/plugin/rest/entity/SessionsCount.java +++ b/src/java/org/jivesoftware/openfire/plugin/rest/entity/SessionsCount.java @@ -16,6 +16,8 @@ package org.jivesoftware.openfire.plugin.rest.entity; +import io.swagger.v3.oas.annotations.media.Schema; + import javax.xml.bind.annotation.XmlElement; import javax.xml.bind.annotation.XmlRootElement; @@ -23,6 +25,7 @@ * The Class SessionsCount. */ @XmlRootElement(name = "sessions") +@Schema(description = "The number of client sessions.") public class SessionsCount { /** The local sessions. */ @@ -55,6 +58,7 @@ public SessionsCount(int localSessions, int clusterSessions) { * @return the local sessions */ @XmlElement() + @Schema(description = "The number of authenticated client sessions (of both anonymous and non-anonymous users) on the cluster node that processes the request.", example = "12") public int getLocalSessions() { return localSessions; } @@ -74,6 +78,7 @@ public void setLocalSessions(int localSessions) { * @return the cluster sessions */ @XmlElement() + @Schema(description = "The number of authenticated client sessions (of both anonymous and non-anonymous users) in the entire cluster.", example = "30") public int getClusterSessions() { return clusterSessions; } diff --git a/src/java/org/jivesoftware/openfire/plugin/rest/entity/SystemProperties.java b/src/java/org/jivesoftware/openfire/plugin/rest/entity/SystemProperties.java index e8ca969f0..7ea50bcd2 100644 --- a/src/java/org/jivesoftware/openfire/plugin/rest/entity/SystemProperties.java +++ b/src/java/org/jivesoftware/openfire/plugin/rest/entity/SystemProperties.java @@ -16,6 +16,10 @@ package org.jivesoftware.openfire.plugin.rest.entity; +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.ArraySchema; +import io.swagger.v3.oas.annotations.media.Schema; + import java.util.List; import javax.xml.bind.annotation.XmlElement; @@ -25,6 +29,7 @@ * The Class SystemProperties. */ @XmlRootElement(name = "properties") +@Schema(description = "A list of Openfire system properties.") public class SystemProperties { /** The properties. */ @@ -43,6 +48,8 @@ public SystemProperties() { * @return the properties */ @XmlElement(name = "property") + @JsonProperty(value = "property") + @ArraySchema(arraySchema = @Schema(description = "The system properties.")) public List<SystemProperty> getProperties() { return properties; } diff --git a/src/java/org/jivesoftware/openfire/plugin/rest/entity/SystemProperty.java b/src/java/org/jivesoftware/openfire/plugin/rest/entity/SystemProperty.java index 6e80ba055..1dcf52304 100644 --- a/src/java/org/jivesoftware/openfire/plugin/rest/entity/SystemProperty.java +++ b/src/java/org/jivesoftware/openfire/plugin/rest/entity/SystemProperty.java @@ -16,6 +16,8 @@ package org.jivesoftware.openfire.plugin.rest.entity; +import io.swagger.v3.oas.annotations.media.Schema; + import javax.xml.bind.annotation.XmlAttribute; import javax.xml.bind.annotation.XmlRootElement; @@ -23,6 +25,7 @@ * The Class SystemProperty. */ @XmlRootElement(name = "property") +@Schema(description = "An Openfire system property.") public class SystemProperty { /** The key. */ @@ -55,6 +58,7 @@ public SystemProperty(String key, String value) { * @return the key */ @XmlAttribute + @Schema(description = "The name of the system property.", example = "xmpp.domain", requiredMode = Schema.RequiredMode.REQUIRED) public String getKey() { return key; } @@ -75,6 +79,7 @@ public void setKey(String key) { * @return the value */ @XmlAttribute + @Schema(description = "The value of the system property.", example = "example.org", requiredMode = Schema.RequiredMode.REQUIRED) public String getValue() { return value; } diff --git a/src/java/org/jivesoftware/openfire/plugin/rest/entity/UserEntities.java b/src/java/org/jivesoftware/openfire/plugin/rest/entity/UserEntities.java index dba05f60c..a5d92d856 100644 --- a/src/java/org/jivesoftware/openfire/plugin/rest/entity/UserEntities.java +++ b/src/java/org/jivesoftware/openfire/plugin/rest/entity/UserEntities.java @@ -17,6 +17,8 @@ package org.jivesoftware.openfire.plugin.rest.entity; import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.ArraySchema; +import io.swagger.v3.oas.annotations.media.Schema; import java.util.List; @@ -27,6 +29,7 @@ * The Class UserEntities. */ @XmlRootElement(name = "users") +@Schema(description = "A list of Openfire users.") public class UserEntities { /** The users. */ @@ -56,6 +59,7 @@ public UserEntities(List<UserEntity> users) { */ @XmlElement(name = "user") @JsonProperty(value = "users") + @ArraySchema(arraySchema = @Schema(description = "The users.")) public List<UserEntity> getUsers() { return users; } diff --git a/src/java/org/jivesoftware/openfire/plugin/rest/entity/UserEntity.java b/src/java/org/jivesoftware/openfire/plugin/rest/entity/UserEntity.java index 223257889..ce6505862 100644 --- a/src/java/org/jivesoftware/openfire/plugin/rest/entity/UserEntity.java +++ b/src/java/org/jivesoftware/openfire/plugin/rest/entity/UserEntity.java @@ -17,6 +17,8 @@ package org.jivesoftware.openfire.plugin.rest.entity; import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.ArraySchema; +import io.swagger.v3.oas.annotations.media.Schema; import java.util.List; @@ -30,6 +32,7 @@ */ @XmlRootElement(name = "user") @XmlType(propOrder = { "username", "name", "email", "password", "properties" }) +@Schema(description = "An Openfire user.") public class UserEntity { /** The username. */ @@ -76,6 +79,7 @@ public UserEntity(String username, String name, String email) { * @return the username */ @XmlElement + @Schema(description = "The username of the user. Required when creating a user. When updating a user, providing a different username renames the user.", example = "john") public String getUsername() { return username; } @@ -96,6 +100,7 @@ public void setUsername(String username) { * @return the name */ @XmlElement + @Schema(description = "The name of the user.", example = "John Doe") public String getName() { return name; } @@ -116,6 +121,7 @@ public void setName(String name) { * @return the email */ @XmlElement + @Schema(description = "The email address of the user.", example = "john@example.org") public String getEmail() { return email; } @@ -135,6 +141,7 @@ public void setEmail(String email) { * * @return the password */ + @Schema(description = "The password of the user. Required when creating a user. Never included in responses.", example = "s3cr3t") public String getPassword() { return password; } @@ -157,6 +164,7 @@ public void setPassword(String password) { @XmlElement(name = "property") @XmlElementWrapper(name = "properties") @JsonProperty(value = "properties") + @ArraySchema(arraySchema = @Schema(description = "Custom properties of the user. Property keys are unique per user. When updating a user, all existing properties of the user are replaced by the provided properties: omitting this removes all properties of the user.")) public List<UserProperty> getProperties() { return properties; } diff --git a/src/java/org/jivesoftware/openfire/plugin/rest/entity/UserGroupsEntity.java b/src/java/org/jivesoftware/openfire/plugin/rest/entity/UserGroupsEntity.java index f507c70bf..8e304326e 100644 --- a/src/java/org/jivesoftware/openfire/plugin/rest/entity/UserGroupsEntity.java +++ b/src/java/org/jivesoftware/openfire/plugin/rest/entity/UserGroupsEntity.java @@ -17,6 +17,8 @@ package org.jivesoftware.openfire.plugin.rest.entity; import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.ArraySchema; +import io.swagger.v3.oas.annotations.media.Schema; import java.util.List; @@ -28,6 +30,7 @@ * The Class UserGroupsEntity. */ @XmlRootElement(name = "groups") +@Schema(description = "A list of names of Openfire user groups.") public class UserGroupsEntity { /** The group names. */ @@ -57,6 +60,7 @@ public UserGroupsEntity(List<String> groupNames) { */ @XmlElement(name = "groupname") @JsonProperty(value = "groupnames") + @ArraySchema(arraySchema = @Schema(description = "The names of the groups."), schema = @Schema(example = "Sales")) public List<String> getGroupNames() { return groupNames; } diff --git a/src/java/org/jivesoftware/openfire/plugin/rest/entity/UserProperty.java b/src/java/org/jivesoftware/openfire/plugin/rest/entity/UserProperty.java index ceb48aa7c..bf5dcbcab 100644 --- a/src/java/org/jivesoftware/openfire/plugin/rest/entity/UserProperty.java +++ b/src/java/org/jivesoftware/openfire/plugin/rest/entity/UserProperty.java @@ -16,11 +16,14 @@ package org.jivesoftware.openfire.plugin.rest.entity; +import io.swagger.v3.oas.annotations.media.Schema; + import javax.xml.bind.annotation.XmlAttribute; /** * The Class UserProperty. */ +@Schema(description = "A custom property of an Openfire user.") public class UserProperty { /** The key. */ @@ -55,6 +58,7 @@ public UserProperty(String key, String value) { * @return the key */ @XmlAttribute + @Schema(description = "The key (name) of the property. Unique per user.", example = "department", requiredMode = Schema.RequiredMode.REQUIRED) public String getKey() { return key; } @@ -75,6 +79,7 @@ public void setKey(String key) { * @return the value */ @XmlAttribute + @Schema(description = "The value of the property.", example = "Sales", requiredMode = Schema.RequiredMode.REQUIRED) public String getValue() { return value; } diff --git a/src/java/org/jivesoftware/openfire/plugin/rest/exceptions/ErrorResponse.java b/src/java/org/jivesoftware/openfire/plugin/rest/exceptions/ErrorResponse.java index 4e8f2b1fa..e467cfd24 100644 --- a/src/java/org/jivesoftware/openfire/plugin/rest/exceptions/ErrorResponse.java +++ b/src/java/org/jivesoftware/openfire/plugin/rest/exceptions/ErrorResponse.java @@ -16,6 +16,8 @@ package org.jivesoftware.openfire.plugin.rest.exceptions; +import io.swagger.v3.oas.annotations.media.Schema; + import javax.xml.bind.annotation.XmlElement; import javax.xml.bind.annotation.XmlRootElement; @@ -23,6 +25,7 @@ * The Class ErrorResponse. */ @XmlRootElement(name = "error") +@Schema(description = "A description of an error that occurred while processing a request.") public class ErrorResponse { /** The resource. */ @@ -43,6 +46,7 @@ public class ErrorResponse { * @return the resource */ @XmlElement(name = "resource") + @Schema(description = "The resource (for example, a username or room name) that the error relates to.", example = "john") public String getResource() { return resource; } @@ -62,6 +66,7 @@ public void setResource(String resource) { * @return the message */ @XmlElement(name = "message") + @Schema(description = "A description of the error.", example = "Could not get user") public String getMessage() { return message; } @@ -81,6 +86,7 @@ public void setMessage(String message) { * @return the exception */ @XmlElement(name = "exception") + @Schema(description = "The type of the error.", example = "UserNotFoundException") public String getException() { return exception; } @@ -100,6 +106,7 @@ public void setException(String exception) { * @return the exception stack */ @XmlElement(name = "exceptionStack") + @Schema(hidden = true) public String getExceptionStack() { return exceptionStack; } diff --git a/src/java/org/jivesoftware/openfire/plugin/rest/service/UserVCardService.java b/src/java/org/jivesoftware/openfire/plugin/rest/service/UserVCardService.java index e8cfa4687..fdac4c8ae 100644 --- a/src/java/org/jivesoftware/openfire/plugin/rest/service/UserVCardService.java +++ b/src/java/org/jivesoftware/openfire/plugin/rest/service/UserVCardService.java @@ -19,6 +19,7 @@ import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.ExampleObject; import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.parameters.RequestBody; import io.swagger.v3.oas.annotations.responses.ApiResponse; @@ -84,7 +85,22 @@ public String getUserVcard( @Consumes({MediaType.APPLICATION_XML}) public Response setUserVcard( @Parameter(description = "The username of the user for which to update the vCard.", required = true) @PathParam("username") String username, - @RequestBody(description = "The updated definition of the vCard.", required = true) String vCard) + @RequestBody(description = "The updated definition of the vCard, in the vcard-temp format of XEP-0054.", required = true, + content = @Content(mediaType = MediaType.APPLICATION_XML, schema = @Schema(type = "string"), examples = @ExampleObject(value = + "<vCard xmlns=\"vcard-temp\">\n" + + " <FN>Janice Francis Doe</FN>\n" + + " <N>\n" + + " <FAMILY>Doe</FAMILY>\n" + + " <GIVEN>Janice</GIVEN>\n" + + " <MIDDLE>Francis</MIDDLE>\n" + + " </N>\n" + + " <NICKNAME>Jane</NICKNAME>\n" + + " <EMAIL>\n" + + " <INTERNET/>\n" + + " <PREF/>\n" + + " <USERID>j.doe@example.org</USERID>\n" + + " </EMAIL>\n" + + "</vCard>"))) String vCard) throws ServiceException { plugin.setUserVCard(username, vCard); From 72032ddb27135266b9f9af550af25db5d35c6afb Mon Sep 17 00:00:00 2001 From: Guus der Kinderen <guus.der.kinderen@gmail.com> Date: Fri, 25 Sep 2026 21:09:32 +0200 Subject: [PATCH 4/4] Generate readme.html after the generated documentation has been added to readme.md The documentation of endpoints and data types in readme.md is generated in the 'process-classes' phase. The generation of readme.html used to happen earlier (in the 'generate-resources' phase), which caused readme.html to be based on outdated documentation after a change to the API. It now happens in the 'prepare-package' phase. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- pom.xml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/pom.xml b/pom.xml index 8108c4258..f7be766f7 100644 --- a/pom.xml +++ b/pom.xml @@ -101,14 +101,15 @@ </executions> </plugin> - <!-- Generate readme.html from readme.md, so that the two can't get out of sync. --> + <!-- Generate readme.html from readme.md, so that the two can't get out of sync. This is done in the 'prepare-package' + phase, after readme.md has been updated with the generated documentation (in the 'process-classes' phase). --> <plugin> <!-- Isolate readme.md, to prevent other (untracked) markdown files in the project root from being rendered and packaged. --> <artifactId>maven-resources-plugin</artifactId> <executions> <execution> <id>copy-readme-markdown</id> - <phase>generate-sources</phase> + <phase>prepare-package</phase> <goals> <goal>copy-resources</goal> </goals> @@ -134,7 +135,7 @@ <executions> <execution> <id>generate-readme-html</id> - <phase>generate-resources</phase> + <phase>prepare-package</phase> <goals> <goal>generate</goal> </goals>