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

bind()

BindModule

sasl()

SASLModule

sasl2()

SASL2Module

discovery()

DiscoveryModule

capabilities()

EntityCapabilitiesModule

presence()

PresenceModule

roster()

RosterModule

SaslModule & Sasl2Module

SaslModule and Sasl2Module provides mechanism to authenticate user in XMPP server. Our current implementation supports a set of mechanisms to do that:

  • SCRAM-SHA-1 & SCRAM-SHA-1-PLUS

  • SCRAM-SHA-256 & SCRAM-SHA-256-PLUS

  • SCRAM-SHA-512 & SCRAM-SHA-512-PLUS

  • PLAIN

All SCRAM mechanisms with PLUS allow to bind authentication process with TLS channel. It makes authentication process more secure and protect against man-in-the-middle attack.

SCRAM mechanisms in Halcyon supports three types on channel binding: tls-unique, tls-exporter and tls-server-end-point. Unfortunately, because of limitation of Java TLS API implementation, by default only tls-server-end-point is supported.

To enable other channel binding types, you have to use BouncyCastle based TLS processor. It is provided by separate module, so you need to import it to your project

implementation("tigase.halcyon:halcyon-bouncycastle:$HalcyonVersion")

and configure socket connector:

import tigase.halcyon.core.builder.createHalcyon
import tigase.halcyon.core.builder.socketConnector
import tigase.halcyon.core.connector.socket.BouncyCastleTLSProcessor

val halcyon = createHalcyon {
    socketConnector {
        tlsProcessorFactory = BouncyCastleTLSProcessor
   }
}

SaslModule vs Sasl2Module

Sasl2Module does exactly the same what SaslModule. The only difference is that Sasl2Module is used with Bind 2 mechanism.

Events

SASLEvent has three subtypes of events:

  • SASLStarted fired when authentication process begins, it also provides used mechanism;

  • SASLSuccess fired when authentication process is finished with success;

  • SASLError fired on authentication error, it provides sasl error type and optional human readable description.

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 = "[email protected]".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 = "[email protected]".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("[email protected]".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 that presence 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 = "[email protected]".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 = "[email protected]".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("[email protected]".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 and ItemRemoved.

  • 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 and clientType 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 and clientVersion 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 uses https://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 to false.

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.