Tigase Halcyon XMPP Library
Welcome
Halcyon is an XMPP client library written in a Kotlin programming language. It provides implementation of core of the XMPP standard and processing XML. Additionally it provides support for many popular extensions (XEP’s).
Library using Kotlin Multiplatform feature to provide XMPP library for as many platforms as possible. Currently we are focused on
JVM
JavaScript
Android
In the future we want to provide native binary version.
Getting started with a Halcyon
Setting up a client
Supported platforms
Halcyon library can be used on different platforms:
JVM
Android
JavaScript
Adding client dependencies
To use Halcyon library in your project you have to configure repositories and add library dependency. All versions of library are available in Tigase Maven repository:
- Production
repositories { maven("https://maven-repo.tigase.org/repository/release/") }
- Snapshot
repositories { maven("https://maven-repo.tigase.org/repository/snapshot/") }
At the end, you have to add dependency to tigase.halcyon:halcyon-core
artifact:
implementation("tigase.halcyon:halcyon-core:$halcyon_version")
Where $halcyon_version
is required Halcyon version.
Creating and configuring a client
When repositories and dependencies are configured, we can create instance of Halcyon:
import tigase.halcyon.core.builder.createHalcyon
val halcyon = createHalcyon {
}
Authentication
Of course, it requires a bit of configuration: to connect to XMPP Server, client requires username and password:
import tigase.halcyon.core.builder.createHalcyon
import tigase.halcyon.core.xmpp.toBareJID
val halcyon = createHalcyon {
auth {
userJID = "username@xmppserver.com".toBareJID()
password { "secretpassword" }
}
}
Registering new account
To register new account on XMPP server you need separate instance of Halcyon, configured exactly for this purpose.
import tigase.halcyon.core.builder.createHalcyon
val halcyon = createHalcyon {
register {
domain = "xmppserver.com"
registrationFormHandler { form ->
form.getFieldByVar("username")!!.fieldValue = "username"
form.getFieldByVar("password")!!.fieldValue = "password"
}
}
}
Note
The server may provide a different set of fields and it is the developer’s responsibility to handle them.
Connectors
Halcyon library is able to use many connection methods, depends on platform. By default JVM and Android uses Socket and JavaScript uses WebSocket connector.
JVM SocketConnector
In Socket Connector you may configure own DNS resolver, set custom host and port, and define trust manager to check SSL server certificates.
import tigase.halcyon.core.builder.createHalcyon
import tigase.halcyon.core.builder.socketConnector
val halcyon = createHalcyon {
socketConnector {
dnsResolver = CustomDNSResolver()
hostname = "127.0.0.1"
port = 15222
trustManager = MyTrustManager()
}
}
Warning
Note that by default Halcyon doesn’t check SSL server certificates at all!
JavaScript WebSocketConnector
If your target platform is JavaScript, then default connector will use WebSocket.
import tigase.halcyon.core.builder.createHalcyon
import tigase.halcyon.core.builder.webSocketConnector
val halcyon = createHalcyon {
webSocketConnector {
webSocketUrl = "ws://127.0.0.1:5290/"
}
}
WebSocket connector has only one configuration parameter: server URL.
Starting and stopping
Now we are ready to connect client to the XMPP server:
halcyon.connectAndWait()
halcyon.disconnect()
Method connectAndWait()
is JVM only method, it esteblish connection in blocking way. To start connection in async mode you have to use connect()
method.
If library was configured to register new account, thise method will start registration process.
Method disconnect()
terminates XMPP session, closes streams and sockets.
Connection status
We can listen for changing status of connection:
halcyon.eventBus.register<HalcyonStateChangeEvent>(HalcyonStateChangeEvent.TYPE) { stateChangeEvent ->
println("Halcyon state: ${stateChangeEvent.oldState}->${stateChangeEvent.newState}")
}
Available states:
Connecting
- this state means, that methodconnect()
was called, and connection to server is in progress.Connected
- connection is fully established.Disconnecting
- connection is closing because of error or manual disconnecting.Disconnected
- Halcyon is disconnected from XMPP server, but it is still active. It may start reconnecting to server automatically.Stopped
- Halcyon is turned off (not active).
Events
Halcyon is events driven library. It means you have to listen for events to receive message, react for disconnection or so. There is single events bus inside, to which you can register listeners. Each part of library (like modules, connectors, etc.) may have set of own events to fire.
General code to registering events:
halcyon.eventBus.register<EVENT_TYPE>(EVENT_NAME) { event ->
…
}
In Halcyon, name of event is defined as constant variable named TYPE
in each event.
For example:
halcyon.eventBus.register<ReceivedXMLElementEvent>(ReceivedXMLElementEvent.TYPE) { event ->
println(" >>> ${event.element.getAsString()}")
}
You can use EventBus for you own applications. No need to register
events types. Just create object inherited from
tigase.halcyon.core.eventbus.Event
and call method
eventbus.fire()
:
data class SampleEvent(val sampleData: String) : Event(TYPE){
companion object {
const val TYPE = "sampleEvent"
}
}
halcyon.eventBus.fire(SampleEvent("test"))
Modules
Architecture of Halcyon library is based on plugins (called modules). Every feature like authentication, sending and receiving messages or contact list management is implemented as module. Halcyon contains all modules in single package (at least for now), so no need to add more dependencies.
To install module you have to use install
function:
import tigase.halcyon.core.builder.createHalcyon
import tigase.halcyon.core.xmpp.modules.discovery.DiscoveryModule
val halcyon = createHalcyon {
install(DiscoveryModule)
}
Most of modules can be configured. Configuration may be passed in install
block:
import tigase.halcyon.core.builder.createHalcyon
import tigase.halcyon.core.xmpp.modules.discovery.DiscoveryModule
val halcyon = createHalcyon {
install(DiscoveryModule) {
clientName = "My Private Bot"
clientCategory = "client"
clientType = "bot"
}
}
By default, function createHalcyon()
automatically add all modules. If you want to configure your own set of modules, you have to disable this feature and add required plugins by hand:
import tigase.halcyon.core.builder.createHalcyon
import tigase.halcyon.core.xmpp.modules.discovery.DiscoveryModule
val halcyon = createHalcyon(installAllModules = false) {
install(DiscoveryModule)
install(RosterModule)
install(PresenceModule)
}
Note
Despite of the name, with install
you can also configure preinstalled modules!
Halcyon modules mechanism is implementing modules dependencies, it means that if you install module (for example) MIXModule
, Halcyon automatically install modules RosterModule
, PubSubModule
and MAMModule
with default configuration.
There is also set of aliases, to make configuration of popular modules more comfortable.
import tigase.halcyon.core.builder.createHalcyon
import tigase.halcyon.core.builder.bind
val halcyon = createHalcyon() {
bind {
resource = "my-little-bot"
}
}
In this example we used bind{}
instead of install(BindModule){}
.
List of aliases:
Alias |
Module name |
---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
PresenceModule
Module for handling received presence information.
Install and configure
To install or configure preinstalled Presence module, call function install
inside Halcyon configuration (see Modules):
import tigase.halcyon.core.builder.createHalcyon
import tigase.halcyon.core.xmpp.modules.presence.PresenceModule
import tigase.halcyon.core.xmpp.modules.presence.InMemoryPresenceStore
val halcyon = createHalcyon {
install(PresenceModule) {
store = InMemoryPresenceStore()
}
}
The only one configuration property store
allows to use own implementation of presence store.
Setting own presence status
After connection is established, PresenceModule automatically sends initial presence.
To change own presence status, you should use sendPresence function.`
import tigase.halcyon.core.xmpp.modules.presence.PresenceModule
halcyon.getModule(PresenceModule)
.sendPresence(
show = Show.Chat,
status = "I'm ready for party!"
)
.send()
It is also possible to send direct presence, only for specific recipient:
import tigase.halcyon.core.xmpp.modules.presence.PresenceModule halcyon.getModule(PresenceModule) .sendPresence( jid = "mom@server.com".toJID(), show = Show.DnD, status = "I'm doing my homework!" ) .send()
Presence subscription
Details of managing presence subscription are explained in XMPP specification. Here we simply show how to subscribe and unsubscribe presence with Halcyon library.
All subscriptions manipulation may be done with single sendSubscriptionSet function:
import tigase.halcyon.core.xmpp.modules.presence.PresenceModule halcyon.getModule(PresenceModule) .sendSubscriptionSet(jid = "buddy@somewhere.com".toJID(), presenceType = PresenceType.Subscribe) .send()
Depends on action you want, you have to change presenceType
parameter:
- Subscription request:
Use
PresenceType.Subscribe
to send subscription request to given JabberID.- Accepting subscription request:
Use
PresenceType.Subscribed
to accept subscription request from given JabberID.- Rejecting subscription request:
Use
PresenceType.Unsubscribed
to reject subscription request or cancelling existing subscription to our presence from given JabberID.- Unsubscribe contact:
Use
PresenceType.Unsubscribe
to cancel your subscription of given JabberID presence.
Note
Remember that subscription manipulation can affect your roster content.
Checking presence
When you develop application, probably you will want to check presence of your contact, to see if he is available.
Halcyon provides few function for that: getBestPresenceOf
returns presence with highest priority (in case if there are few entities under the same bare JID);
getPresenceOf
returns last received presence of given full JID. You can also check list of all entities resources logged as single bare JID with getResources
function.
Because determining of contact presence using low-level XMPP approach is not so intuitive, we introduced TypeAndShow
. It joins presence stanza type and show extension in single set of enums.
import tigase.halcyon.core.xmpp.modules.presence.PresenceModule import tigase.halcyon.core.xmpp.modules.presence.typeAndShow val contactStatus = halcyon.getModule(PresenceModule) .getBestPresenceOf("dad@server.com".toBareJID()) .typeAndShow()
Thanks to it, contactStatus
value will contain easy to show contact status like online, offline, away, etc.
Events
Module can fire two types of events:
PresenceReceivedEvent
is fired when any Presence stanza is received by client. Event contains JID of sender, stanza type (copied from stanza) and whole received stanza.ContactChangeStatusEvent
is fired when received stanza changes contact presence (all subscriptions requests are ignored). Event contains JID of sender, human readable status description, current presence with highest priority and just received presence stanza. Note thatpresence
in this event may contain stanza received long time ago. Current event is caused by receiving presence from entity with lower priority.
RosterModule
Module for managing roster (contact list).
Install and configure
To install or configure preinstalled Roster module, call function install
inside Halcyon configuration (see Modules):
import tigase.halcyon.core.builder.createHalcyon
import tigase.halcyon.core.xmpp.modules.roster.RosterModule
import tigase.halcyon.core.xmpp.modules.roster.InMemoryRosterStore
val halcyon = createHalcyon {
install(RosterModule) {
store = InMemoryRosterStore()
}
}
The only one configuration property store
allows to use own implementation of roster store.
Retrieving roster
When connection is established, client automatically requests for latest roster, so no additional actions are required.
Most modern XMPP server supports roster versioning. Thanks to it, client do not have to receive whole roster from server (which can be large). So we recommend, to implement own RosterStore to keep current roster content between client launches.
Manipulating roster
To add new contact to you roster you have to call addItem
function:
import tigase.halcyon.core.xmpp.modules.roster.RosterModule
import tigase.halcyon.core.xmpp.modules.roster.RosterItem
halcyon.getModule(RosterModule)
.addItem(
RosterItem(
jid = "contact@somewhere.com".toBareJID(),
name = "My friend",
)
)
.send()
Warning
Remember, that (as described in RFC) after call (and send) roster modification request, your local store will not be updated immediately. Roster store is updated only on server request!
When roster item is saved in your roster store, Halcyon fires RosterEvent.ItemAdded
event.
To modify existing roster item, you have to call exactly the same addItem
function:
import tigase.halcyon.core.xmpp.modules.roster.RosterModule
import tigase.halcyon.core.xmpp.modules.roster.RosterItem
halcyon.getModule(RosterModule)
.addItem(
RosterItem(
jid = "contact@somewhere.com".toBareJID(),
name = "My best friend!",
)
)
.send()
The difference is that after local store update Halcyon fires RosterEvent.ItemUpdated
event.
Last thing is removing items from roster:
import tigase.halcyon.core.xmpp.modules.roster.RosterModule
import tigase.halcyon.core.xmpp.modules.roster.RosterItem
halcyon.getModule(RosterModule)
.deleteItem("contact@somewhere.com".toBareJID())
.send()
When item will be removed from local store, Halcyon fires RosterEvent.ItemRemoved
event.
Events
Roster module can fires few types of events:
RosterEvent
is fired when roster item in your local store is modified by server request. There are three sub-events:ItemAdded
,ItemUpdated
andItemRemoved
.RosterLoadedEvent
inform us that roster data loading is finished. It is called only after retrieving roster on client request.RosterUpdatedEvent
is fired, when processing roster data from server is finished. I will be triggered after requesting roster from server and after processing set of roster item manipulations initiated by server.
DiscoveryModule
This module implements XEP-0030: Service Discovery.
Install and configure
To install or configure preinstalled Discovery module, call function install
inside Halcyon configuration (see Modules):
import tigase.halcyon.core.builder.createHalcyon
import tigase.halcyon.core.xmpp.modules.discovery.DiscoveryModule
val halcyon = createHalcyon {
install(DiscoveryModule) {
clientCategory = "client"
clientType = "console"
clientName = "Code Snippet Demo"
clientVersion = "1.2.3"
}
}
The DiscoveryModule
configuration is provided by interface DiscoveryModuleConfiguration
.
The
clientCategory
andclientType
properties provides information about category and type of client you develop.
List of allowed values you can use is published in Service Discovery Identities document.
The
clientName
andclientVersion
properties contains human readable software name and version.
Note
If you change client name and version, it is good to update node
name in EntityCapabilitiesModule.
Discovering information
Module provides function info
to prepare request to get information about given entity:
import tigase.halcyon.core.xmpp.modules.discovery.DiscoveryModule
import tigase.halcyon.core.xmpp.toJID
halcyon.getModule(DiscoveryModule)
.info("tigase.org".toJID())
.response { result ->
result.onFailure { error -> println("Error $error") }
result.onSuccess { info ->
println("Received info from ${info.jid}:")
println("Features " + info.features)
println(info.identities.joinToString { identity ->
"${identity.name} (${identity.category}, ${identity.type})"
})
}
}
.send()
In case of success, module return DiscoveryModule.Info
class containing information about requested JID and node, list of received identities and list of features.
Discovering list
Second feature provided by module is discovering list of items associated with an entity. It is implemented by the items
function:
import tigase.halcyon.core.xmpp.modules.discovery.DiscoveryModule
import tigase.halcyon.core.xmpp.toJID
halcyon.getModule(DiscoveryModule)
.items("tigase.org".toJID())
.response { result ->
result.onFailure { error -> println("Error $error") }
result.onSuccess { items ->
println("Received info from ${items.jid}:")
println(items.items.joinToString { "${it.name} (${it.jid}, ${it.node})" })
}
}
.send()
In case of success, module return DiscoveryModule.Items
class containing information about requested JID, node and list of received items.
Events
After connection to server is established, module automatically requests for for features of user account and server.
When Halcyon receives account information, then AccountFeaturesReceivedEvent
event is fired. In case of receiving XMPP server information, Halcyon fires ServerFeaturesReceivedEvent
event.
EntityCapabilitiesModule
This module implements XEP-0115: XMPP Ping.
Install and configure
To install or configure preinstalled EntityCapabilities module, call function install
inside Halcyon configuration (see Modules):
import tigase.halcyon.core.builder.createHalcyon
import tigase.halcyon.core.xmpp.modules.caps.EntityCapabilitiesModule
val halcyon = createHalcyon {
install(EntityCapabilitiesModule) {
node = "http://mycompany.com/bestclientever"
cache = MyCapsCacheImplementation()
storeInvalid = false
}
}
The EntityCapabilitiesModule
configuration is provided by interface EntityCapabilitiesModuleConfig
.
The
node
is URI to identify your software. As default library useshttps://tigase.org/halcyon
URI.With
cache
propery you can use own implementation of capabilities cache store, for example JDBC based, to keep all received capabilities between your application restarts.The
storeInvalid
property allow to force storing received capabilities with invalid versification string. By default, it is set tofalse
.
For more information about the possible consequences of disabling the validation verification string, refer to the Security Considerations chapter.
Getting capabilities
You can get entity capabilities based on the presence received.
import tigase.halcyon.core.xmpp.modules.caps.EntityCapabilitiesModule
val caps = halcyon.getModule(EntityCapabilitiesModule)
.getCapabilities(presence)
The primary use of the module is to define a list of features of the client with whom communication is taking place. After receiving presence from client we can determine features implemented by it:
import tigase.halcyon.core.xmpp.modules.caps.EntityCapabilitiesModule
val caps = halcyon.getModule(EntityCapabilitiesModule)
.getCapabilities(presence)
PingModule
This module implements XEP-0199: XMPP Ping.
Install
To install or configure preinstalled Discovery module, call function install
inside Halcyon configuration (see Modules):
import tigase.halcyon.core.builder.createHalcyon
import tigase.halcyon.core.xmpp.modules.PingModule
val halcyon = createHalcyon {
install(PingModule)
}
This module has no configuration options.
Note
If module will not be installed, other entities will not be able to ping application.
Pinging entity
Module provides function ping
to prepare request for ping given entity:
import tigase.halcyon.core.xmpp.modules.PingModule
import tigase.halcyon.core.xmpp.toJID
halcyon.getModule(PingModule)
.ping("tigase.org".toJID())
.response { result ->
result.onSuccess { pong -> println("Pong: ${pong.time}ms") }
result.onFailure { error -> println("Error $error") }
}
.send()
In the case of success, module returns PingModule.Pong class containing information about measured response time.
Requests
Each module may perform some requests on other XMPP entities, and (if yes) must return RequestBuilder
object to allow check status of request and receive response.
For example, suppose we want to ping XMPP server (as described in XEP-0199):
Sample ping request and response.
<!-- Client sends: -->
<iq to='tigase.net' id='ping-1' type='get'>
<ping xmlns='urn:xmpp:ping'/>
</iq>
<!-- Client receives: -->
<iq from='tigase.net' to='client@tigase.net' id='ping-1' type='result'/>
There is module PingModule
in Halcyon to do it:
import tigase.halcyon.core.xmpp.modules.PingModule
val pingModule: PingModule = client.getModule(PingModule)
val request = pingModule.ping("tigase.net".toJID()).send()
In this case, method ping()
returns RequestBuilder
to allow add result handler, change default timeout and other operations. To send stanza you have to call method ‘send()’. There is also available method build()
what also creates request object, but doesn’t sends it.
Note
On JVM, methods of handler will be called from separate thread.
Most universal way to receive result in asynchronous way is add response handler to request builder:
val client = Halcyon()
val pingModule: PingModule = client.getModule(PingModule)
pingModule.ping("tigase.net".toJID()).response { result ->
result.onSuccess { pong ->
println("Pong: ${pong.time}ms")
}
result.onFailure { error ->
println("Error $error")
}
}.send()
Jabber Data Form
Jabber Data Form is described in XEP-0004. Data forms are useful in all workflows not described in XEPs. For example service configuration or search results.
Working with forms
To access fields of received form, we have to create JabberDataForm
object:
val form = JabberDataForm(formElement)
Where formElement
is representation of <x xmlns='jabber:x:data'>
XML element.
Each form may have properties like:
type
- form type,title
- optional title of form,description
- optional, human-readable, description of form.
Fields are identified by var
name. Each field may have field type (it is optional).
Let look, how to list all fields with values:
val form = JabberDataForm(element)
println("Title: ${form.title}")
println("Description: ${form.description}")
println("Type: ${form.type}")
println("Fields:")
form.getAllFields().forEach {
println(" - ${it.fieldName}: ${it.fieldType} (${it.fieldLabel}) == ${it.fieldValue}")
}
To get field by name, simple use:
val passwordField = form.getFieldByVar("password")
Value of those fields may be modified:
passwordField.fieldValue = "******"
After all form modification, sometimes we need to send filled form back. There is separated method to prepare submit-ready form:
val formElement = form.createSubmitForm()
This method prepares <x xmlns='jabber:x:data'>
XML element with type submit
and all fields are cleared up from unnecessary elements like descriptions or labels. It just leaves simple filed with name and value.
Creating forms
We can create new form, set title and description, and add fields:
val form = JabberDataForm.create(FormType.Form)
form.addField("username", FieldType.TextSingle)
form.addField("password", FieldType.TextPrivate).apply {
fieldLabel = "Enter password"
fieldDesc = "Password must contain at least 8 characters"
fieldRequired = true
}
To get XML element containing form without cleaning it, just use:
val formElement = form.element
Multi value response
There is a variant of form containing many sets of fields. This kind of form has declared set of column with names and set of items containing field with names declared before.
This example shows how to display all fields with values:
val form = JabberDataForm(element)
val columns = form.getReportedColumns().mapNotNull { it.fieldName }
columns.forEach { print("$it; ") }
println()
println("------------")
form.getItems().forEach { item ->
columns.forEach { col -> print("${item.getValue(col).fieldValue}; ") }
println()
}
Creating multi value form is also simple. First we have to set list of reported columns, because when new item is added, field names are checked against declared columns.
val form = JabberDataForm.create(FormType.Result)
form.title = "Bot Configuration"
form.setReportedColumns(listOf(Field.create("name", null), Field.create("url", null)))
form.addItem(
listOf(Field.create("name").apply { fieldValue = "Comune di Verona - Benvenuti nel sito ufficiale" },
Field.create("url").apply { fieldValue = "http://www.comune.verona.it/" })
)
form.addItem(
listOf(Field.create("name").apply { fieldValue = "Universita degli Studi di Verona - Home Page" },
Field.create("url").apply { fieldValue = "http://www.univr.it/" })
)