Unichat API


This is a new service, an API-based fast and lightweight chat service, compatible with the legal requirements of most countries. The chat has a core service, chat engine, that can be used in any Universa-connected projects freely.

The API is an RPC interface available through different media and connections, such as native sockets, web sockets and the HTTP REST endpoint, which all share access to the same RPC interface, documented below.

Each RPC call has a procedure name and a hash (aka map/dictionary) of named arguments, and returns another suchmap of named parameters, possibily empty, or error information.

Currently, Unichat API could be accessed using:

The latter uses the SSE mechanism to send the notifications.

API Conventions

The API functions are specified in the following form:

some_method({foo: <string>,bar: <timestamp>}) -> {result: <string>}

Which means:

  • the callable method some_method (with websock unichat endpoint also available as someMethod)
  • it takes 2 named arguments, foo of type string and bar of type timestamp which all are explained in detail later
  • it returns a single named value, result, which is a string.

Unichat API data types

The api is multiplatform and so are its data types.

name type
<int> integer signed value of 32 bits
<long> integer signed value of 64 bits
<decimal_string> decimal value (like float number but of higher precision)
<timestamp> date and time in ISO 8601: Extended Format

If the type is prepended with opt_ prefix, e.g. <opt_string>, it means that corresponding parameter may be omitted or missing.

Common methods

These are general-purpose methods:

ping

ping(string: <string>) -> {pong: string}

Use it to test the connections. There is, basically, no need to ping the connection to keep it alive unless specified in the connection endpoint.

version

version() -> {version: <string>, api_level: <int>, min_api_level: <int> }

Additional to the well-known version string, this call returns 2 numbers that are of importance to the application programmer:

  • api_level: current api level, the number that grows every time the API suffers significant change. When it is greater than one the application expects, it is time to consult the docs and see what has been changed and might needed to be updated in the applicaiton code.

  • min_api_level: the minumum api level which is generally compatible with current state of the API.

While we will do our best to make API as backward compatible as possible, with time some things may require incompatible changes, so the application detected the min_api_level being above its expectation should ask user to updgrade immediately.

User methods

Chat User record

Is returned in may chat API objects. The own record has more fields as most usr information is not disclosed to others.

{
  "user": {
    "id": 290,
    "nick": "test_user_1",
    "email": "[email protected]", // private fields:
    "confirmed_at": "2018-10-14T22:40:55Z"   
     "auth_token": "ZWuzrbsnXivG6J1wrhagWEhDDR_kfigGzIOCRbyLNA_ZiYBDepngE2Xv2",
     "avatar": {"full":<string_url>,"small":<string=_url>} // or null
  }

fields marked as private are only shown to the user in me call. The rest are public fields visible to anybody. If confirmed_at is null, the email confirmation should be passed as soon as possible. Avatar, if present, has always 2 images: full and probably reduced, could be the same if original image was too small.

Registration

register(email:<string>,password:<string>) -> {user: <chat user record>}

If email is invalid or already taken, returns error with code: 'invalid_email'. Any other registration error return code registration_error wil a text contained detailed error description. Also too weak password may cause errors.

It user is registered successfully, the chat user record is returned and an email with confirmation instruction is sent. User must be advised to confirm it as soon as possible as most methods required confirmed email.

Resending confirmation instructinos

In the case user has not received it, it is allowed to send instructions, not too often.

resend_confirmation(email: <email>) -> {}

returns empty hash on success. Could return error code: 'please_wait' if reconfirmation was sent recently.

Logging in

Password login

login(email: <string>,password:<string>) -> { user: <chat_user_record> }

Typical error codes are auth_failed and please_wait if called too frequently.

Token login

It is possible to login also with the auth_token returend in own chat user record:

login(auth_token:<auth_token_string>) -> { user: <chat_user_record> }

The auth_token is returned in own iser record returned by login() and me() calls.

Logging out

If for some user application needs to leg out current user (logging in required)

logout() -> {}

Update self

Requires logged in user.

update_me(nick: <opt_string>,password: <opt_string>,
          current_password: <opt_string>,
          avatar: <data_url>)
    -> { user: <user_record> }

Any parameter may be missing. Only and all fields presented in arguments will be changed. Any number of fields could be changed with the single call. If a null value will be passed or field will not be listed in arguments, it won't be changed.

The password should _always_ be accompained withcurrentpasswordor error withinvalidcode` password will be returned.

Typical errors are: invalid_params if a not allowed field presented and invalid_nick if the new nick is bad or in use. If the error occurs, no data will be changed. Nick limitations as for now are:

  • nick must start with a letter
  • nick can not have spaces before or after (will be stripped if any)
  • nick should be at least 5 characters long
  • nick should not contain more than one consequent spaces

Successful profile update sends notification

   {event:"changed",object_type:"user",object:{<user_record>}

to all relevant users including self, e.g. to all users that have groups in common with this one (and thus may require redraw on clients).

Avatar format

The avatar optional parameter described above must be a data url string, containig all the fields, e.g. data:<mediatype>;base64,<data_base64>. No part could be omitted. mediatype should be image/..., we support png and jpeg for sure and many other formats that we do not reccomend to use.

It should not be too big, we will impose restrictions in near future, the avatar should be below 2M anyway.

Get self

Requires logged in user.

me() -> { user: <user_record> }

Get the SSE authorization code

This code is only needed to get SSE events, which could be used with HTTP REST endpoint. With Websock endpoint it is useless as events are already transmitted via same connection with API calls.

To create/get existing code:

api_create_sse_auth_code() -> { code: <string_code> }

The code then could be used with http rest unichat endpoint to subscribe to SSE events.

Delete SSE authorization code

When the clietn software does not want to be able to receive SSE events it could revoke the code by calling:

api_delete_sse_auth_code() -> {}

Get user

Use it to refresh other user public data. As for now, all this information is already included in contacts/subscriptions.

get_user(user_id:<long>) -> { user: <user_record> }

This method does not require authentication.

Contacts

Contacts are represented by <contact_record>:

{
  "contact": {
    "blocked_at": null,         // not null if blocked
    "confirmed_at": null,       // not null if confirmed
    "created_at": "2018-10-14 23:10:10 UTC",
    "updated_at": "2018-10-14 23:10:10 UTC",
    "user": {
      "id": 517,
      "nick": "test_user_2"
    }
  }

Adding contact

add_contact(email: <opt_string>,nick: <opt_string>,user_id: <opt_long>) 
    -> {contact: <contact_record>}

Only one of email, user_id or nick must be presented.

On success, return contact record. If no contact found returns code: not_found error.

The just added contact is in "unconfirmed state". It will become confirmed when other party will confirm it by adding it to contacts.

Other party will see the nick of the party which has called add_contact first and therefore could use it to add it back. When both perties have added each other, the contact state will turn to confirmed.

Unconfirmed contacts should be shown differently and could have different notifications policy from confirmed ones.

Another way of adding contact is creating a provate chat to it. It will add the contact automatically.

List contacts

get_contacts() -> { contacts: [<contact_reocrd>,...] }

deprecated form also works:

contacts() -> { contacts: [<contact_reocrd>,...] }

but we ask to remove it from code, as it will be removed soon.

Get last activity time

get_last_active_at(user_id: <long>) -> { last_active_at: <time_string> }

returns last known activity time of the user, or null of current user has no access to this information.

Private chats

These are special kind of unichat groups reserved for pribate conversations. Private chats do not allow administration and inviting participants. Only one private chat could exist for a given pair of users.

Create/get private chat

get_private_subscription(user_id: <long>) -> {subscription: <subscription_record>}

Returns subscription to the private group for current and specified user, creating it if need. See also unichat subscriptions. The returned structure may look like:

{
  "subscription": {
    "id": 6062,
    "group_id": 1028,
    "user_id": 3768,
    "role": "rw",
    "mute_until": null,
    "draft": null,
    "last_read_message_id": null,
    "created_at": "2018-10-23T13:57:10Z",
    "group": {
      "id": 1028,
      "name": null,
      "is_deleted": false,
      "created_at": "2018-10-23 13:57:10 UTC",
      "updated_at": "2018-10-23 13:57:10 UTC",
      "type": "private_chat",
      "icon": null,
      "participants": [
        {
          "id": 6062,
          "group_id": 1028,
          "user_id": 3768,
          "role": "rw",
          "mute_until": null,
          "draft": null,
          "last_read_message_id": null,
          "created_at": "2018-10-23T13:57:10Z",
          "user": {
            "id": 3768,
            "nick": "test_user_1",
            "avatar": null
          }
        },
        {
          "id": 6063,
          "group_id": 1028,
          "user_id": 3769,
          "role": "rw",
          "mute_until": null,
          "draft": null,
          "last_read_message_id": null,
          "created_at": "2018-10-23T13:57:10Z",
          "user": {
            "id": 3769,
            "nick": "test_user_2",
            "avatar": null
          }
        }
      ]
    },
    "user": {
      "id": 3768,
      "nick": "test_user_1",
      "avatar": null
    }
  }
}

Notice subscription.group.type above.

There are always only 2 participants and chat type is private_chat. Use it as a usual unichat group to write, list and change messages.

Important. UI can not delete private chat group or unsubscribe from it. Instead, party can block the other one, mute notifications to acheive same effect. We advise just not to show private chats for blocked users.

Groups

Please read unichat groups and unichat subscriptions first to clearly understand what are these and how it works.

Get subscriptions

For each group used is subscribed to there is a corresponding subscription which carries a group information plus user rights in the group and more important data, see more in unichat subscription.

So, instead of querying groups user has access to, client calls for subscriptions:

get_subscriptions(short: false,limit: 0,offset: 1000) 
    -> { subscriptions: [<unichat subscription record>,...]

See unichat subscription record for the record structure, and unichat groups and unichat subscriptions for overall explanation.

  • This command supports paging. Sunscriptions are ordered by subscription.id ascending, so just set limit and offset to some non-default values to iterate through subscriptions.

  • set short: true to get subscription information without group participants. the short form carries all the information to show groups bar, while heavy group.participants section is needed only when the group is opened.

We recommend first to scan subscriptions by chunks of 10-100 records in short mode to quickly display them, and use get_subscription(id) when opening a group UI, or scan them all in the background wither way.

Get single subscription

Use it when you need to get or refresh only one subscription with known id, for exmaple, after receiving the notification.

get_subscription(subscription_id:<long>) 
    -> {subscription: <subscription_record>} 

It is not equal to get_subscriptions() as it provides access to the goven id that can't be done otherwise without scanning all (in worst case) subscriptions.

Create room

A room is a general purpose chat. Creator becomes owner and admins. As ususal with universa chat, the result is the subscription of the creator.

create_room name: <string>, user_ids: []
    -> { subscription: <unichat subscription record>}

with parameters:

  • name: required room name. Any string.
  • userids: optional array of `userid` to be invited immediately.

It is possible to create room without users: it will create only creator (which becomes an owner) subscription.

It returns the creator's unichat subscription record as {subscription: <unichat subscription record>}. Creator becomes an admin, all invited users will get write permissions. IT is possible to add participants later.

When creating subscriptions for the room, each involved user, including creator, will receive new subscription notificaton, e.g.

{"event": "new", "object_name": "subscription", "object": {<subscription record>}}

Also, new created room will be posted with one unichat system message with xtag: 'creation' from room owner and one xtag: 'invite', reference_type: 'user', reference_id: user_id for each invited user.

Message record

Carries information about teh chat message. It can be short and long. Short version represents deleted messages, where text and some other fieds are not available. Here is the example:

{
  "message": {
    "id": 34,
    "user_id": 367,
    "group_id": 54,
    "deleted_at": null, // meaning it is not deleted
    "serial": 38,
    "text": "hello u2!",
    "xtag": null,
    "attachment": null,
    "mentions": [], // or null, or array of mention records
    "reference": {type: "user", id: "1"},
    "forwarded_message_id": 11,   // optional: only if not null
    "in_reply_to_message_id": 22, // optional: only if not null
    "edited_at": null,  // was not edited
    "created_at": "2018-10-15T18:33:56Z"
  }
}

reply and forward

If the message is the forward of another message, it will have forwarded_message_id field pointing to the original message. Same way in_reply_to_message_id if exists, points to the original message to which this one is a reply. To create forward/reply just add correspodning ids when posting the message.

Message serial

Every time message is changed, say, its text is modified, its serial field gets some new value, which is guaranteed to be bigger than it was. This way the new and changed messages coudl easily be loaded by calling all messages with serial bigger than the last (e.g. greatest) known to the client.

This query will automatically add new and changed messages alltogether.

xtag

String tag user with unichat system messages to specify type of special message.

Reference

With some unichat system messages referenced are used to specify some connected object, in which case its type and id are passed in this field.

Attachment

If message has attachment, it will be included into message record with the attachment key. for example:

"attachment": {
  "content_type": "image/jpeg",
  "byte_size": 1597,                // size in bytes
  "url": <download_url_string>,     // fownload link
  "preview": <preview_url_string>   // only for images
},

The attachment could be of any type, but previews are only available for messages.

Post message

User can post messages to groups where it is subscribed and has write permission.

post(group_id:<opt_long>,subscription_id:<opt_long>,text:<string>,
    uid:<string>, attachment:<opt_string>, in_reply_to_message_id: <opt_long>,
    forwarded_message_id: <opt_long>, 
    mentions: [{user_id:<long>,text: <string>},...])
    ->{message: <message_record>}

Parameters are:

  • group_id OR subscription_id: one of two is required. Call it wuth subscripion whereever possible to reduce server load.

  • text: message text, required as for now.

  • uid: some generally unique identifier, random string of at least 48 characters is advised. Maximum allowed size is 64 characters. Can use GUID though we do not recommend it as most RNG give better entropy. uid must be unique for a user ofr medium time intervals.

  • attachment: if present, must be a valid and full data-url string, e.g. data:<mediatype>;base64,<data>. For mediatypes image/* the system will prepare also preview image automaticlly.

  • in_reply_to_message_id: if present, must point to the original message to which new one will reply.

  • forwarded_to_message_id: if present, must point to the original message to which new one will reply.

  • 'mentions': optional array of mentions, see below. important do not pass null, just omit this field if not needed.

Returned value is a {message: <message record>} containing a created message.

It is safe to call it repeatedly with the same message and same uid, no duplication will happen and the proper message object will be returned.

Creating messages cause notification to be sent to all group members, therefore, the postin user will also receive it. Notification will arive at any time, before (unlikely) or after the call returns.

Important note. Notification passed to the message owner connection will contain object.uid field first few hours after message creation at least. Other recipients or past the time this field could be null or omitted.

Mentions

Mentions are @somebody-style mentions of some group participant in the message. The client must detect them (could be in any form) and fill the mentions array as stated above, where text is a subsctring in the source message, that the client could use to highlight or substitute to the link, and user_id is the id of the mentioned user as it was when the message was comosing.

This was different clients could properly show and process mentions despite on the format and algorythm of mentions entering/detecting, and does not depends on the users changing their nicks in futire.

Mentions are reported in the message record as .mention array. Also the system will set subscription.last_mentioned_in_message_id accordingly. This chainge of the subscription will not trigger subscription change notification, as the recipient will already get the new message notification, which will contain mentions so the client software could derive the necessary information of it, so issuing separate subscription change is redundant.

Why uid?

When client software attempts to post a message, it may happen it will not arrive to the service, or the service couls be in error state, ir, worst of all, the client may not receive acknowledgment that the message was actually created.

When client software does not receive answer for the post() call, it should retry until succeeded. It may therefore cose unintentional message duplication when the server has actually posted the message but the client software did not receive result. Simplest is the user has get out of mobile internet coverage.

To avoid it, the client software should generate a more or less unique random string, uid and store it locally with the message, posting it on every try. This way the system will detect and ignore unintentional duplications.

Load messages

This function allow reading exisitng messages in a group with paging in tow modes: get most recently created and get created and added after some point. It requires authenticaion.

get_messages(subscription_id:<opt_long>,group_id:<opt_long>,
            limit:100,offset:0, before_id:<opt_long>,after_serial:<opt_long) 
    -> { messages: [<message_record>,...] }

Parameters are:

  • group_id or subscription_id: the group to read messages from, current user must have read access to it. Please use subscription_id where possible.
  • limit and offset allow paging in usual sense
  • before_id if present, select messages that are older than a given id, in most recent first.
  • after_serial if present, select messages that are created and modified path one with such serial number, most recent last.
  • if neither before_id not after_serial are specified, selects most recently created messages, most recent first.

In other words, to get latest messages, specify no selection arguments and use offset, otherwise use before_id which is roughly the same. If you want to pull all the messages and their changes, pull it all using after_serial using the biggest serial you have preloaded, and you will get them all and most recent versions of them too.

Get single message

It may happen, for example, when received new message notification, get a message just by its id, bypassing looking up the subscription for its group:

get_message(message_id:<long>) -> {message:<message_record>}

Edit own message

Editing own messages is only allowed within certain time period after its creation. Message editing does not prolong this period. Edited messages has non-null last_edited_at field so edited messages can be shown in a different way. The system does not keep the message edition history, so it is the only evidence that the text was changed.

edit_message(message_id:<long>, text:<opt_string>, attahcment: <opt_string> )
    -> {message: <message_record>}

requires at least one of attachment and text. Use empty string for each to clear it without deleting the message. I we not recommend to leave empty messages, the system might decide to delete completely empty messages of regular type.

  • text: if present, chagnges message text. Pass empty string to delete the text only. Pass null to leave text unchanfed.
  • attachment: if present, changes the attachment. must be a valid and full data-url string, e.g. data:<mediatype>;base64,<data>. For mediatypes image/* the system will prepare also preview image automaticlly. Use empty string "" to delete attachment keeping the message. Pass null leave attachment unchanged.

It will broadcast notification of message change to a group, e.g.

{event: 'changed', object_type: 'message', object: <message_record>}

Notice that the updated message has increased serial field value (by some unknown positive number), so it is possible to get new and edited messages alltogether as descibed above in "load messages".

Delete own message

Could be done by the author at any time. This operation is irreversible.

delete_message(message_id: <long>)
    -> {message: <message_record>

If the message is already deleted, it is not changed, error is not reported.

Note that the deleted message record contain less information: it has no text, attachemt, edited_at and created_at fields.

It will broadcast notification of message deletion to a group, e.g.

{event: 'deleted', object_type: 'message', object: <message_record>}

Manage own subscription

Susbscrption have several fields writable by its owner that allow implement better UX:

  • last_read_message_id: set it to the message id that was likely read by the user. Setting this field may notify other group members about it. Note that service does not check the value against message ids so you can write there zero, negative value and whatever you might find useful.
  • draft: save here the text entered by the user to not to loose it. Convenient to share partially written message among sessions and devices.
  • mute_until: if set to some time, notifications to this subscription should not be shown to the user clearly. This setting can only be interpretated by the client software.

To change it:

update_subscription(subscription_id:<long>,draft:<opt_string>,
                    last_read_message_id:<opt_long>,
                    mute_until:<opt_is08601_datetime>)
    -> { subscription: <subscription_record> }

Only supplied fields will be changed. If subscription is changes, changed notification for it will be sent to the owner.

To clear mute_until set it to any moment in past. Setting to null will not change its value.

Setting last_read_message_id will also clear last_mentioned_in_message_id if it is lesser or equal to newly set last_read_message_id.

Invite to the room

Regular rooms by default allow everybody with write permission to invite others to the room with:

invite(subscription_id:<long>,user_id:<long>) 
    -> {subscription:<subscription_record>}

where

-subscription_id is a subscription of the current user to the room to which he or she wants to invite

  • user_id: id of the user which should be invited to the room.

on success, returns the subscription of newly added user, send a notification of the new subscription and post a unichat system message from inviter with xtag:"invite" and reference pointing to the new user. Of course posting message causes also new message notification.

Use invite code

If the user has the invite code, it should use it. See unichat invite codes for explanations on how to obtain and process it with the client software.

use_invite_code(code:<string_code>) -> {}

There is no returned data on successful call. Instead, the application will receive notifications and messages depedning on the code.

When user joins the group using the invite code, all participants (include new one) will receive usual new subscription event. Also system posts unichat system messsage from new user with xtag:"joined".

Leaving the room

To do it, just unsubscribe your subscription:

unsubscribe(subscription_id:<long>) -> {}

On successful unsubscription, the notification od deleted objecy is propagated among group subscribers:

{
    event:'deleted', 
    object_name:'subscription',
    object: {id: deleted_subscription_id}
}

Notice that on the deleted object notificatoin only id field is guaranteed to be presented, othe fields may be all omitted.

Also, system posts unichat system message from leaving user with xtag: leave just before unsusbcribing, so the leaving user will receive its notification.

Please note that after unsubscribing the user may loose access to the group entirely depending on its nature and settings.

When the group owner leaves

the group can not exist without owner, who is its unrevokable admin. The only way for owner to dismiss is to call unsibscribe. It is not possible to remove owner's admin role or kick him or her out of teh group. So when the owner leaves, the group risks to get the failed state with no admins left.

Som when the owner leaves by hos good will, the system tries to find best candidate on this role (oldest admin. oldest writer, oldest reader without mutes and bans), and assign it on this role. The subscription of the new role is promoted to the admin level if need. The usual subscription change notification is sent.

When the last participant leves

the group is destroyed by the system if there are no more participants. No archived groups exist at this time.

Administration

The user having subscription with admin role can administrate the group with methods described below.

Change other user role

Unless other user is a group owner, admin can change other users roles, also promoting them to admin (this could be changed in group settings later).

set_access(subscription_id:<long>,user_id:<long>,role:<string)
    ->{subscription: <updated_user_subscription> }

Where subscription_id is the subscription of the admin to the group to update and user_id is the user to update access. Valid roles are: ro, rw and admin.

When access is changed, temporary readonly mode and temporary ban are cleared.

Owner access can not be changed.

Kick user from a group

kick(subscription_id:<long>,user_id:<long>)
    -> {}

Where subscription_id is the subscription of the admin to the group to change.

It is not allowed to kick owner out of the group. Other admins can be kicked out as well.

Successful kick sends system message xtagged kick_out.

Change group appearance

update_group(subscription_id:<long>,name:<opt_string>,icon: <opt_string>)
    -> { group: <unichat group record> }
  • name: if not empty, the group name will be changed.
  • icon: if not empyy. the group icon will be reset. Icon data should be a complete image data url, e.g. data:image/<subtype>;base64,<data>. We recommend use only jpeg for photo images and png for graphic art.

See unichat subscription record for group record sample.

Create group invite code

Group admin can create a code that allow anybody to join the group. See unichat invite codes for how to use them.

create_group_invite_code(subscription_id:<long>)
    -> { code: <string> }

If code is already set, this call returns it. To change code, administrator must delete existing code first, them create new one.

The invite code is also available to all participants, if present, it will be included in every group object as:

group: {
    // ...
    invite_code: <string>
    // ...
}

This field will be set to null if the code is not set/deleted. The field could be also missing in some circumstances.

Delete group invite code

Group admin can delete the invite code (see unichat invite codes]):

delete_group_invite_code(subscription_id:<long>) -> {}

If there was no code, it does nothing.

Unichat notifications

Whenever something happen on the server, it notifies relevant clients about it by sending the notification.

Receiving notifications

The client side receives notifications in the RPC it could provide. The signature of the noticiation target should be (javascript example, in localInterface object):

onNotificationReceived: function({notifications:...}) {
}

This function, it presented in localInterface will be called by the server each time a relevant event occur. The functions receives notifications named argument, and recevies named argument notifcations with array of notification records explained below.

Notification data

Each notification has 3 mandatory fields:

  • event: could be new, changed, or deleted, or some exotic types discussed later on.
  • object_type: the type of the referenced object
  • object: the referenced object with minimum information about it, as we want to reduce the size of the events stream. Client can load object in full details with corresponding API methods as need.

Here is the real world example:

{
  "notifications": [
    {
      "event": "new",
      "object_type": "message",
      "object": {
        "id": 8,
        "user_id": 72,
        "group_id": 2,
        "deleted_at": null,
        "serial": 8,
        "text": "hello world ;)",
        "edited_at": null,
        "created_at": "2018-10-15T17:54:41Z"
      }
    }
  ]
}

Note that notifications come in Array, most often, of 1 element size. Still it may happen to have more than 1. In that case, first item of the array represent the most old notification, and the last - the most recent one.

Special notifications

Events that are not just changed state of some object are described here.

"entering": user enters text

Is emitted to all group participants when some user saved a draft of a message to his subscription. To trigger it. just save currently entered text into a draft in the subscription. Client sofwtare should do it by the inactivity timer when user has entered/changed anything in the chat message fileld.

Notification has following format:

    {
        event:       "entering",
        object_name: "no matter", // ignore it
        object: {
            user_id:383,          // user who is typing message
            group_id:78           // group where it occurs
        }
    }