5. Modules
Halcyon contains set of modules responsible for implementation of various tasks: sending and receiving messages, authentication, pinging, uploading files, handling roster, presences, etc.
Modules:
5.1. BindModule
Resource Binding module. This module is responsible for resource binding as described in RFC.
5.1.1. Properties
boundJID
- contains full JID bound during resource binding process, or null if client isn’t logged in and/or not binded.
5.1.2. Methods
There is no reason to call methods from this module in client. This module is used internally by Halcyon library.
bind(String)
Method prepares request to bind resource. As parameters it gets proposed
resource name or null
if resource name should be generated by
server. In response it returns object BindResult
contains full bound
JID.
5.2. DiscoveryModule
This module implements XEP-0030: Service Discovery.
5.2.1. Properties
There are few properties to set in this module:
clientName
- Client name.clientVersion
- Version of client.clientCategory
- Category of client.clientType
- Type of client.
Category and type of client are described in Service Discovery Identities document.
5.2.2. Events
ServerFeaturesReceivedEvent
Fired when server features, to where client is connected, are received. Client asks for server features automatically during login process.
AccountFeaturesReceivedEvent
Fired when user account features are received. Client asks for those features automatically.
5.2.3. Methods
info(JID, String)
Method prepares disco#info
request. As parameters it takes JID of entity and node name. Both are optional. In response returns object Info
contains JID of entity, node name, list of identities and list of features.
items(JID, String)
Method prepares disco#items
request. As parameters it takes JID of entity and node name. Both are optional. In response returns object Items
contains JID of entity, node name and list of items.
findComponent((Info) → Boolean, (Info) → Unit)
This method may be used to find component on currently connected server with specific features or type.
As first parameter it takes condition checker, which much check if given Info
is this object what we are searching for. Second parameter is consumer.
findComponent({ candidate ->
candidate.identities.any { it.type == "mix" }
}) { result ->
println("${result.jid}")
}
5.3. MessageCarbonsModule
This module implements XEP-0280: Message Carbons.
5.3.1. Events
Sent
Fired when client receives carbon of message sent by other entity using the same account. Event contains carboned message.
Received
Fired when client receives carbon of message received by other entity using the same account. Event contains carboned message.
5.3.2. Methods
enable()
Method prepares request to enable carbon messages in current session.
disable()
Method prepares request to disable carbon messages in current session.
5.4. PingModule
This module implements XEP-0199: XMPP Ping. It allows to ping XMPP entities over XML stream.
5.4.1. Published features
urn:xmpp:ping
5.4.2. Methods
ping(JID)
This method prepares ping request. Response object Pong
contains measured round-trip time in miliseconds.
5.5. PresenceModule
Module for handling received presence information.
5.5.1. Events
PresenceReceivedEvent
Fired when any Presence stanza is received by client.
Fields:
jid
- Stanza sender JID.stanzaType
- Presence stanza type.stanza
- Whole received presence stanza.
ContactChangeStatusEvent
Fired when Presence stanza is received but it contains different set of fields:
jid
- Bare JID of contact.status
- Human readable status set by contact.presence
- Current “best” presence stanza, based on presence priority.lastReceivedPresence
- 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.
5.5.2. Methods
getPresenceOf(jid: JID)
Returns presence of given entity or null if never received presence from this entity.
getBestPresenceOf(jid: BareJID)
Returns best known presence of given bare JID.
sendPresence(jid: JID?, type: PresenceType?, show: Show?, status: String)
Sends presence stanza to specific jid.
sendSubscriptionSet(jid: JID, presenceType: PresenceType)
Method for quick send response for subscription request.
5.6. RosterModule
This module implements XEP-0060: Publish-Subscribe. It adds publish-subscribe functionality.
5.6.1. Events
PubSubEventReceivedEvent
5.6.2. Methods
create(pubSubJID: JID, node: String, configForm: JabberDataForm? = null)
fun subscribe(pubSubJID: JID, node: String, jid: JID)
fun purgeItems(pubSubJID: JID, node: String)
fun retrieveSubscriptions(pubSubJID: JID, node: String)
fun modifySubscriptions(pubSubJID: JID, node: String, subscriptions: List<Subscription>)
fun deleteItem(jid: JID, node: String, itemId: String)
fun retrieveItem(jid: JID, node: String, itemId: String? = null)
fun publish(jid: JID?, node: String, itemId: String?, payload: Element? = null)
fun retrieveAffiliations(jid: JID?, node: String? = null)
5.7. RosterModule
Module is reponsible for keeping and managing roster items.
5.7.1. Events
ItemAdded
Fired when new item is added to roster.
ItemUpdated
Fired when item is modified.
ItemRemoved
Fired when item is removed from roster.
5.7.2. Methods
addItem(vararg items: RosterItem)
Method prepares request which add or update item to roster. When server confirms action, event ItemAdded
or ItemUpdated
will be fired.
deleteItem(vararg jids: BareJID)
Method prepares request to remove item from roster. When server confirms operation, event ItemRemoved
will be fired.
getAllItems()
Method returns add known roster items.
5.7.3. Implementing own storage
RosterModule supports roster versioning, but it requires custom implementation of RosterStore
to allow storing roster locally. By default, Halcyon has in-memory roster store. To do that, there is interface tigase.halcyon.core.xmpp.modules.roster.RosterStore
what need to be extend. To use custom implementation of RosterStore
simply put it to property store
in RosterModule
. Note, that it have to be done before login.
5.8. SASLModule
Module is responsible for whole client authentication process.
5.8.1. Properties
saslContext
contains context of module. Context is cleared when connection is started. It has several fields to read:mechanism
- used SASL mechanism,state
- current state of authentication process,complete
-true
if authentication process is finished (it doesn’t matter with success or with error).
5.8.2. Events
SASLStarted
Fired when authentication process begins.
Fields:
mechanism
- name of used SASL mechanism.
SASLSuccess
Fired when authentication is successful.
SASLError
Fired when authentication finished with error.
Fields:
error
- enum with type of error. SASL errors are described if RFC.description
- human readable description of error (if provided by server).
5.8.3. Methods
There is no reason to call methods from this module in client. This module is used internally by Halcyon library.
startAuth()
This method begins authentication process. It doesn’t return Request object.
5.9. VCardModule
This module allows to publishing and retrieving VCard4 as described in XEP-0292.
5.9.1. Properties
autoRetrieve
- Iftrue
then module automatically retrieve VCard before firingVCardUpdatedEvent
. It isfalse
by default.
5.9.2. Events
VCardUpdatedEvent
Fired when VCard update is received from PEP. Contains JID that the update applies to. If autoRetrieve
is set to true
then event will contains current VCard.
5.9.3. Methods
retrieveVCard(jid: BareJID)
This method prepare request for retrieving VCard of given JID. As result returns VCard
object.
fun publish(vcard: VCard)
This method prepare request for publishing own vcard.
5.9.4. Usage example
Retrieving VCard
val vCardModule = halcyon.getModule<VCardModule>(VCardModule.TYPE)!!
vCardModule.retrieveVCard("[email protected]".toBareJID()).response { result ->
result.onSuccess {vcard->
println("""
Received vcard:
Name: ${vcard.formattedName}
Name: ${vcard.structuredName?.given} ${vcard.structuredName?.surname}
Nick: ${vcard.nickname}
Birthday: ${vcard.birthday}
TimeZone: ${vcard.timeZone}
""".trimIndent())
println()
vcard.addresses.forEach {addr->
println("""
${addr.street}
${addr.locality} ${addr.region} ${addr.code}
${addr.country}
""".trimIndent())
}
}
result.onFailure {
println("Cannot retrieve VCard. Error: $it")
}
}.send()
Publishing VCard
Publishing own vcard is very simple:
val vCardModule = halcyon.getModule<VCardModule>(VCardModule.TYPE)!!
vCardModule.publish(vcard).response { result ->
result.onSuccess { println("VCard published") }
result.onFailure { println("VCard NOT published") }
}.send()
VCard object is mutable and can be edited. To create new VCard instance you can use VCard builder:
val vCard = vcard {
structuredName {
given = "Alice"
surname = "Carl"
}
nickname = "alice"
email {
parameters {
pref = 1
+"work"
}
text = "[email protected]"
}
}
5.10. BlockingCommandModule
This module implements XEP-0191: Blocking Command and XEP-0377: Spam Reporting
5.10.1. Events
Blocked
Event fired when new contact was blocked. It contains few properties:
jid
- blocked JabberIDreason
- reason of blocking.text
- optional human-readable description of blocking reason.
Unblocked
Event fired, when contact was unblocked. It contains one property:
jid
- unblocked JabberID
UnblockedAll
Event fired when all contacts are unblocked.
halcyon.eventBus.register<BlockingCommandEvent>(BlockingCommandEvent.TYPE) { event ->
when (event) {
is BlockingCommandEvent.Blocked -> println("$event")
is BlockingCommandEvent.Unblocked -> println("$event")
is BlockingCommandEvent.UnblockedAll -> println("All blocked contacts are unblocked now!")
}
}
5.10.2. Methods
Samples:
retrieveList(JID, String)
Retrieves list of blocked contacts. It allows to get list of blocked list, without information about reason.
halcyon.getModule<BlockingCommandModule>(BlockingCommandModule.TYPE).retrieveList().response {
it.onSuccess {
println("Blocked: $it")
}
it.onFailure { println("Oops!") }
}.send()
block
Blocks given contact.
Params:
jid
- BareJID of contact to blockreason
- reason of blocking (optional, default value isNotSpecified
)text
- optional, human-readable description of blocking
halcyon.getModule<BlockingCommandModule>(BlockingCommandModule.TYPE)
.block("[email protected]".toBareJID(), Reason.Spam, "It is SPAMMER!!!")
.response {
it.onSuccess { println("Done") }
it.onFailure { println("Oops!") }
}.send()
unblock
Unblocks given contact.
Params:
jids
- BareJIDs of contacts to unblock
Note
If jids
will be empty, all blocked contacts will be unblocked!
halcyon.getModule<BlockingCommandModule>(BlockingCommandModule.TYPE)
.unblock("[email protected]".toBareJID())
.response {
it.onSuccess { println("Done") }
it.onFailure { println("Oops!") }
}.send()
unblockAll
Unblocks all blocked contacts.
halcyon.getModule<BlockingCommandModule>(BlockingCommandModule.TYPE)
.unblockAll().response {
it.onSuccess { println("Done") }
it.onFailure { println("Oops!") }
}.send()
5.11. CommandModule
This module implements XEP-0050: Ad-Hoc Commands.
5.11.1. Methods
retrieveCommandList
Retrieves list of commands allowed to execute on given XMPP entity. Because this command is just wrapper to DiscoveryModule.items()
method, in response it returns DiscoveryModule.Items
class.
retrieveCommandInfo
Retrieves detailed information about specified command. Because this command is just wrapper to DiscoveryModule.info()
method, in response it returns DiscoveryModule.Info
class.
executeCommand
This method executes Ad-hoc Command on XMPP entity specified by JabberID.
Arguments:
jid
- Jabber ID of command executor,command
- command name,form
- optional element containing data form,action
- command action,sessionId
- session identifier, if command is executed in session (identifier is generated by executor).
As response method returns object of AdHocResult
class. It contains result form (optional) and status of command execution.
val module = halcyon.getModule<CommandsModule>(CommandsModule.TYPE)
module.executeCommand("[email protected]".toJID(), "configure").response {
it.onSuccess { result ->
println("Status: ${result.status}")
println("Form: ${result.form}")
}
}.send()
If this command creates session, we can simply use data from result
to execute next command in it:
result.form.getFieldByVar("password").fieldValue = "1234"
module.executeCommand(result.jid, result.node, result.form.createSubmitForm(), null, result.sessionId).response {
it.onSuccess { result ->
when(result.status){
Status.Completed -> println("Configured")
Status.Canceled -> println("Command canceled")
Status.Executing -> println("Configuration is not finished yet. Next step in session ${result.sessionId} is required.")
}
}
}.send()
Above example uses default action in second step (null
on arguments list).
registerAdHocCommand(command: AdHocCommand)
In addition to executing commands on other XMPP entities, module allows to register ad-hoc commands, to be executed in client by others.
Command must implement AdHocCommand
interface.
class TestAdHoc : AdHocCommand {
override fun isAllowed(jid: BareJID): Boolean = jid == "[email protected]".toBareJID()
override val node: String = "command-node-name"
override val name: String = "Example command"
override fun process(request: AdHocRequest, response: AdHocResponse) {
response.form = createForm()
response.notes = arrayOf(Note.Info("Everything is OK"))
response.status = Status.Completed
}
}
Above sample command may be executed only by owner@example.com
. It even be hidden on commands list for others.
Created command must be registered in CommandModule
:
module.registerAdHocCommand(TestAdHoc())
Ad-hoc commands supports sessions. Session allows to store some data in session context and creates multiple stages commands. By default sessions are not started automatically. To access to session in command, use method request.getSession()
. This method returns current session context or creates new one if it is necessary.
class SessionTestAdHoc : AdHocCommand {
override fun isAllowed(jid: BareJID): Boolean = jid == "[email protected]".toBareJID()
override val node: String = "example-session-adhoc"
override val name: String = "Example session command"
override fun process(request: AdHocRequest, response: AdHocResponse) {
var counter = request.getSession().values["stage"] as Int? ?: 0
++counter
request.getSession().values["stage"] = counter
if (counter < 3) {
response.notes = arrayOf(Note.Info("Step $counter"))
response.actions = arrayOf(Action.Next)
response.defaultAction = Action.Next
response.status = Status.Executing
} else {
response.notes = arrayOf(Note.Info("Finished"))
response.status = Status.Completed
}
}
}
If response status is Completed
or Canceled
, then session context is destroyed after command execution.
Note
Remember, that single instance of Ad-Hoc Command may process calls from many callers.
5.12. MUCModule
This module implements XEP-0045: Multi-User Chat.
5.12.1. Events
There are two kinds of events in MUCModule
:
Room related events.
Other events
Currently the only event from second category is InvitationReceived
:
halcyon.eventBus.register<MucEvents.InvitationReceived>(MucEvents.TYPE) { event ->
println("${event.invitation.sender} invites you to room ${event.invitation.roomjid}")
}
All room related events contains Room
object, and all have common parent:
halcyon.eventBus.register<MucRoomEvents>(MucRoomEvents.TYPE) { event ->
when(event){
is MucRoomEvents.YouJoined -> println("You joined to room ${event.room.roomJID}")
is MucRoomEvents.OccupantCame -> println("Occupant ${event.nickname} came to ${event.room.roomJID}")
is MucRoomEvents.OccupantLeave -> println("Occupant ${event.nickname} leaves room ${event.room.roomJID}")
// ...
}
}
InvitationReceived
Event fired when an invitation is received. Contains Invitation
object.
YouJoined
Event fired when server responses for join request with success.
YouLeaved
Event fired, when you leave room. It may be confirmation of you leave request, or you are kicked out from room.
JoinError
Event fired when server not accepted join request.
Created
Event informs that room you joined is just created (by join request).
OccupantCame
Informs that new occupant joined to room.
OccupantLeave
Informs that occupant leaves the room.
OccupantChangedPresence
Informs that occupant updated his presence.
ReceivedMessage
Event fired when group chat message from room is received.
5.12.2. Methods
join(roomJID, nickname, password)
Builds join request to MUC Room.
Here is simple example to show how to join to room.
halcyon.eventBus.register<MucRoomEvents.YouJoined>(MucRoomEvents.TYPE) {
println("You joined to room ${it.room.roomJID} as ${it.nickname}")
}
mucModule.join("[email protected]".toBareJID(), "thirdwitch").send()
Note, that because of MUC protocol specificity, confirmation of join will be delivered as separated event.
leave(room: Room)
Builds request to leaves MUC Room.
destroy(room: Room)
Builds room destroy request.
invite(room: Room, invitedJid: BareJID, reason: String? = null)
Builds mediated invitation request.
inviteDirectly(room: Room, invitedJid: BareJID, reason: String? = null)
Builds direct invitation request.
retrieveRoomConfig(room: Room)
Builds retrieve room configuration request. In response it returns data form with configuration.
updateRoomConfig(room: Room, form: JabberDataForm)
Builds update room configuration request.
message(room: Room, msg: String)
Builds group chat message request.
decline(invitation: Invitation, reason: String? = null)
Builds decline request for received invitation
accept(invitation: Invitation, nickname: String)
Builds join request to MUC Room based on received invitation.
retrieveAffiliations(room: Room, filter: Affiliation? = null)
Builds request for retrieve affiliations list from MUC room. In response it returns collection of RoomAffiliation.
updateAffiliations(room: Room, affiliations: Collection<RoomAffiliation>)
Builds request for update affiliations list.
updateRoomSubject(room: Room, subject: String?)
Builds request for set room subject.
ping(room: Room)
Build request for self ping, as described in XEP-0410: MUC Self-Ping (Schrödinger’s Chat).
5.12.3. Store
MUCModule requires Room Storage to store rooms data. By default, Halcyon comes with in-memory store.
To build own Store you have to implement this interface:
interface MUCStore {
fun findRoom(roomJID: BareJID): Room?
fun createRoom(roomJID: BareJID, nickname: String): Room
}
Remember, that MUC protocol is not suitable to mobile clients, so state
of room join (in Room
object) may not represent real state. For example, after reconnection client may keep state Joined
but server received information about disconnection and removes occupant from room. To check if rejoin is required, please use ping(room)
function.
5.13. SIMSModule
This module implements partially XEP-0385: Stateless Inline Media Sharing (SIMS). It describes file sharing metadata.
SIMSModule
isn’t module in Halcyon architecture sense. This is rather collection of extensions to help retrieve and generate SIMS structures.
5.13.1. Methods
getReferenceOrNull()
Method returns Reference
object if exists.
getMediaSharingFileOrNull()
This method (extends Reference
) returns shared file details.
createFileSharingReference()
This method creates complete Reference
object with shared file details.
5.14. MAMModule
This module implements XEP-0313: Message Archive Management.
5.14.1. Events
MAMMessageEvent
This event is fired, when each result for query()
method is received.
This event contains fields:
resultStanza
- whole received Message stanza (response for query),queryId
- query identifier,id
- result identifier,forwardedStanza
- result for query containing stanza from archive and original receiving timestamp
5.14.2. Methods
query(to: BareJID? = null, node: String? = null, rsm: RSM.Query? = null, with: String? = null, start: Long? = null, end: Long? = null)
Main method to retrieve chat history from archive.
Arguments:
to
- JID of MAM component. If null, then default MAM component of user server is used,node
- name of node,rsm
- Result Set Management object,with
- name (JID) of interlocutorstart
,end
- timestamps to filter messages by receive date
All of above argoments may be null
.
retrievePreferences()
Retrieves MAM preferences.
In response, you will get Preferences
object containing:
default
- default behaviour of message archiving:Always
,Never
,Roster
.always
- collection of BareJIDs with whom conversations will always be archived.never
- collection of BareJIDs with whom conversations will never be archived.
updatePreferences(preferences: Preferences)
Updates MAM preferences.
5.14.3. Usage
When client establish connection to server, it should ask server for all messages exchanged with other clients connected to the same account.
It can be done by asking server for all messages since, last received message:
val mamModule = halcyon.getModule<MAMModule>(MAMModule.TYPE)
fun ask(q: RSM.Query? = null) {
mamModule.query(
with = "[email protected]",
start = lastReceivedMessageTimestatmp,
rsm = q
).response { res ->
res.onSuccess {
println("Complete: ${it.complete} :: ${it.rsm}")
if (!it.complete) {
ask(RSM.Query(after = it.rsm!!.last))
}
}
}.consume { forwardedStanza: ForwardedStanza<Message> ->
if (forwardedStanza.stanza.body != null) println(
"FROM MAM | ${forwardedStanza.resultId} ${timestampToISO8601(forwardedStanza.timestamp!!)} ${forwardedStanza.stanza.from}: ${forwardedStanza.stanza.body}"
)
}.send()
}
ask()
Because MAM server has defined maximum amount of returned messages, we have to ask until query is not complete. In example it is done by recurrent execution of method ask()
with filled Result Set Management object. RSM.Query(after = it.rsm!!.last)
means that in result must be included only messages located after last message identifier from currently received package of messages.
Note, that parameter with
of method query()
is optional, so you can ask for all messages since specific time exchanged with specific JID, or you can ask for all messages stored in archive.
The second way to retrieve messages from archive is asking for messages located before or after specific message identifier.
val mamModule = halcyon.getModule<MAMModule>(MAMModule.TYPE)
fun ask(q: RSM.Query? = null) {
mamModule.query(
with = "[email protected]",
rsm = q
).response { res ->
res.onSuccess {
println("Complete: ${it.complete} :: ${it.rsm}")
if (!it.complete) {
ask(RSM.Query(after = it.rsm!!.last))
}
}
}.consume { forwardedStanza: ForwardedStanza<Message> ->
if (forwardedStanza.stanza.body != null) println(
"FROM MAM | ${forwardedStanza.resultId} ${timestampToISO8601(forwardedStanza.timestamp!!)} ${forwardedStanza.stanza.from}: ${forwardedStanza.stanza.body}"
)
}.send()
}
ask(RSM(after = lastKnownMessageId))
To get MAM related message ID, you have to use getStanzaIDBy()
function (As parameter, you have to put name of own account):
val mamMessageId = message.getStanzaIDBy("[email protected]".toBareJID())
It is useful when message is received “in normal way”, so when it is pushed to client by server.
When message is received as result of using method query()
(in consumer), identifier is given in ForwardedStanza
:
val mamMessageId = forwardedStanza.resultId
The same mechanism you can use to load older messages in history (not loaded yet by client):
mamModule.query(
with = "[email protected]",
rsm = RSM.Query(before = "2753e4a8-9150-4e34-8757-4cd5e8419159", max = 20)
).response { res ->
res.onSuccess {
println("Complete: ${it.complete} :: ${it.rsm}")
}
}.consume { forwardedStanza: ForwardedStanza<Message> ->
println(
"FROM MAM | ${forwardedStanza.resultId} ${
timestampToISO8601(
forwardedStanza.timestamp!!
)
} ${forwardedStanza.stanza.from}: ${forwardedStanza.stanza.body}"
)
}.send()
In above example, client is asking for 20 messages located in history before message 2753e4a8-9150-4e34-8757-4cd5e8419159
.
Note
Message Archive returns requested amount of messages. Not every message may contain body to show. Some of those messages may contain only confirmation of message read or other controlling commands.