v.63 by Sergey Chernov
2019-11-17 05:11

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.

Available endpoints

Currently, Unichat API could be accessed using:

The latter uses the SSE mechanism to send the notifications.

To access API the application needd APP_DOMAIN_TOKEN exmplained in chat application domain section.

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 user information is not disclosed to others.

{
  "user": {
    "id": 290,
    "nick": "test_user_1",
    "is_online":false,
    "status":nil,
    // private fields:
    "email": "[email protected]", 
    "searchable_nick": true,
    "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.

You can use is_online attribute to check whether user is online or offline.

Available status attribute values: NULL, "away", "busy" and "do_not_disturb"

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.

Note that after confirming email user will be redirected to the root URL specified in the unichat application domain.

Resending confirmation instructions

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.

Reset password

It is possible to reset the password using the email. The procedure contains following steps:

  1. API requests password reset with request_password_reset(email: <user_email>).
  2. Serive checks the email and send instructions to user by email
  3. User visits the link in the received email
  4. Client applications is called with the URL containing password_reset_token in a query part of the URL.
  5. Client application extracts the link and calls reset_password(token:, new_password:)

Detailed step description follows.

Requesting password reset

request_password_reset(email: <strgin> ) -> {}

Email should be a valid email of existing account of the error will be reported.

Nothe the password reset can't be requested too often or the rate_exceeded error will be returned. The error text will contain time limit for the next password reset request call.

Password reset callback

The application can set the password reset callback separately from its root URL in the unichat application domain settings page. If it is not set, then the root URL will be used.

The reset callback will have query part containing password_reset_token parameter which should be extracted. For example, the callback can be:

https://acme.com/reset_password?password_reset_token=ulkjn5g4rkjbhweh67

in this example the password reset callback in the control panel is set to https://acme.com/reset_password, and the token is ulkjn5g4rkjbhweh67

Note that pasword reset callback URL presented in the unichat application domain control panel must not contain any query part.

Resetting the password

The application extracts the passwort reset token from the callback as described above. Havin this token it should ask user to enter and confirm new password, then call API to reset it:

reset_password(token: <string>,password: <string>) -> {}

After this, the client software can login as usual using the new password.

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>,
searchable_nick:<opt_boolean>,
          current_password: <opt_string>,
          avatar: <data_url>)
    -> { user: <user_record> }

Any parameter may be missing. Only and all fields presented in arguments and not null willbe 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.

For example, to set searchable_nick' to false, useseachable_nick: false, not null.

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

Please note that
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).

User status

Requires logged in user.

update_status(status:<string>)

Possible invalid_user_status error if the value is not NULL, "away", "busy", "do_not_disturb" or "invisible".

If the status is "invisible", then the user has information that he is offline and notifications about this user's activity in the chat are not transmitted.

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() -> {}

Push notifications

A push notification is a message that pops up on a mobile device. Push notifications provide convenience and value to app users. For example, users can receive new messages right on their lock screen

Add devise token

For the service to work, you must send the device token. Use this method when the user logs in.

devise_token(token: <string>, notification_service: <string>) -> { user_devise: <user_devise_record> }

Available values for the notification service:

  • ios_app - use for the Apple mobile devises.

User must be logged in.

Typical errors are: invalid_params if a not allowed field presented and invalid notification_service if the notification service is bad or in use. If the error occurs, no data will be changed.

Remove devise token

To remove a token so that the device stops receiving notifications, use:

remove_devise_token(token: <string>, notification_service: <string>) -> { result: true }

User must be logged in. Use this method before the user logs out.

Typical errors are: invalid_params if a not allowed field presented and invalid notification_service if the notification service is bad or in use. If the error occurs, no data will be changed.

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.

Deleting contact

delete_contact(email: <opt_string>,nick: <opt_string>,user_id: <opt_long>)

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

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

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.

Searching known users

Known usrers are these to whom currently logged in user has at least one group in common, e.g. where both are participants.

To seacrh for a nick knowning it part (not ncecessarily the beginning):

search_knwon(nick_part: <string>) 
    -> { users: [<user_record>,..], overflow: <bool> }

nick_part should not be too short (right now the limit is at least 5 characters). invalid_parameter api error is reported if it is too short.

  • users: array of chat user record, possibly empty
  • overflow: true means that there are more matches that are presented. Narrow the search.

It is possible to search for nicks with at least 5 first characters:

search_user(nick_prefix: <string>) 
    -> { users: [<user_record>,...], overflow:<boolean> }
  • users: array of chat user record, possibly empty
  • overflow: true means that there are more matches that are presented. Narrow the search.

Private chats


These are special kind of unichat groups reserved for private conversations. The private chats cannot be administered (moderated) and do not support inviting participants. Only a single private chat could exist between 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",
    "deleted_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,
            "is_online": false
          }
        },
        {
          "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,
            "is_online": true
          }
        }
      ]
    },
    "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.

Remove from chat list

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

Likewise, to remove from the chat list, you can use:

remove_private_subscription(user_id:<long>) 
    -> { subscription: [<unichat subscription record>,...]

Successful removal subscription sends notification:

{ event: "deleted", object_type: "subscription", object: {<subscription_record> }

Recovering to the chat list occurs while calling get_private_subscription method, or post message to private chat with notification:

{ event: "changed", object_type: "subscription", object: {<subscription_record> }

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,
    "attachments": [],
    "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"
  }
}
  • attachment field is obsolete, use attachments.

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.

Files Attachments

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

"attachments": [
  0 => {
    "content_type": "image/jpeg",
    "byte_size": 1597,                // size in bytes
    "url": <download_url_string>,     // fownload link
    "preview": <preview_url_string>,  // only for images
    "filename":"test.jpg"
  }
],
"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 field is obsolete, but left for compatibility with older versions and contains the latest attachment.

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

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>, attachment_filename:<string>, in_reply_to_message_id: <opt_long>,
    forwarded_message_id: <opt_long>, upload_id: [],
    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.

  • attachment_filename: you can change the name of the transmitted file.

  • 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.

  • upload_id: are identifiers of attachments made through the attachment_by_chunks method. If the string is empty, all attachments will be destroyed. Available: nil, empty string, array, number

  • '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.

Large attachment

If the attachment is large, it is necessary to upload the file in chunks.

attachment_by_chunks(file_chunk:<string>, upload_id:<long>, file_size:<long>, file_checksum:<int>, file_name:<string>, async:<boolean>, chunk_id:<integer>)
    -> {upload_id:<long>, file_size:<long>, file_checksum:<int>, content_type: <opt_string>, voice_recognition: <opt_boolean>}

with parameters:

  • async: file sending format (default: false). If async = true then the chunk_id field is required. Chunks can be sent in a different order. When assembling the file, the pieces will be combined into a single file, sorted by chunk_id in turn. If async = false, then the sending of the next chunk should be done after successfully receiving a response about sending the previous chunk.
  • file_chunk: a chunk of file encoded in Base64. If async=falsе then the size of the chunk should be 64,000 bytes, otherwise if async=true then the chunk should not exceed 1 megabyte (1048576 bytes).
  • upload_id: upload file ID; the first time is NULL, the next time is the value that the method returned
  • file_size: file size in bytes
  • file_checksum: file checksum in CRC32 for file verification
  • file_name: file name to be assigned after it is fully uploaded
  • chunk_id: serial number of the chunk (0, 1, 2... etc.). Only if async=true
  • content_type: if present, sets the content type attribute of the file
  • voice_recognition: true or false. True for speech recognition. See "Voice Recognition" below for more information.
  • subscription_id: only with voice_recognition=true. Subscription where the attachment will be sent.

The file is divided into chunks and uploaded to the server.

Response example:

{ 
  upload_id: 14, 
  file_size: 14243242, 
  file_checksum: 3423521938426
}

where:

  • upload_id: upload file ID
  • file_size: if async=false then the upload file size, otherwise the upload chunk size
  • file_checksum: if async=false then the upload file checksum, otherwise the upload chunk checksum

After the file has been completely uploaded, and the file size and checksum match, the key upload_id will be included into message record. For example:

"upload_id": 14

or as an array, if several attachments are loadedy:

"upload_id": [14, 21]

Voice recognition

You can attach the file as a message with voice recognition. In the method attachment_by_chunks you need to pass additional parameters:

  • voice_recognition is a boolean variable, if TRUE - speech recognition is enable.
  • subscription_id - ID of subscription where the attachment will be sent. It is necessary in order to check whether the user has the right to use the voice recognition function. This rule is set for the organization as a timestamp to which this right exists.

After the file has been uploaded, you can call the voice_recognition to start recognizing audio:

voice_recognition(upload_id:<long>)
    -> { 
        result: {
            status: "pending"
            transcript: NULL,
            error:      NULL,
            created_at: "2019-01-01 01:02:03",
            updated_at: "2019-01-01 01:02:03",
            audio: {
                filename:     "audio.ogg",
                content_type: "audio/ogg",
                byte_size:    "2432543",
                url:          "http://example/audio.ogg",
            }
        }
    }

And call the voice_recognition_result method to get recognition results.

voice_recognition_result(upload_id:<long>)
    -> { 
        result: {
            status: "completed"
            transcript: "Hello world",
            error:      NULL,
            created_at: "2019-01-01 01:02:03",
            updated_at: "2019-01-01 01:02:03",
            audio: {
                filename:     "audio.ogg",
                content_type: "audio/ogg",
                byte_size:    "2432543",
                url:          "http://example/audio.ogg",
            }
        }
    }

If the status = pending, then processing is still in progress.

If the status = failed, the error field must be filled.

If the status = completed, the transcript field must be filled.

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>,
             clear_in_reply_to_message_id: false)
    -> {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.
  • clear_in_reply_to_message_id if set to true, drops in_reply_to_message_id. Note that currently it can not be set to anything but dropped while editing the message.

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 way to share partially written message among sessions and devices. Set to empty string to clear it.

  • tags: allow tag the subscription with an array of string tags. This is per- subscription (not per-group which also exists) tagging, allowing each user keep some information joined with the subscription. Tags are not visible to others.

  • 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 or to the group, depending on change relevance. For example, last_read_message_id causes group broadcasting.

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 message 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 of a deleted object is propagated among the group subscribers:

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

Notice that on the deleted object notification, only the id field is guaranteed to be present, the other fields may be all omitted.

Also, the system posts unichat system message from leaving user with xtag: leave just before unsubscribing, 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

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

So when the owner leaves by their good will, the system tries to find the best candidate for this role (the oldest administrator, or oldest writer or 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>, is_space: <opt_boolean>, pinned_message_id: <opt_long>)
    -> { group: <unichat group record> }
  • name: if not empty, the group name will be changed.
  • icon: if not empty, 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.
  • is_space: if not empty, the group attribute is_space will be changed.
  • pinned_message_id: message ID. If not empty, pinned message will be changed. To delete, use an empty string.

See unichat subscription record for group record sample.

Create group invite code

The group admin can create a code that allow anybody to join the group. See unichat invite codes for details 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 software 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
        }
    }

is_online or is_offline: user status.

Example:

    {
        event:       "is_online",
        object_name: "user",
        object: {
            "id":87,
            "nick":"ivan",
            ...
        }
    }

Organizations

Create organization

Creator becomes owner and admins.

create_organization: name: <string>, icon: <opt_string>, brand_color: <opt_string>
    -> { organization: <organization_record> }

with parameters:

  • name: required organization name. Any string.
  • icon: if not empty, the organization icon will be reset. Icon data should be a complete image data url, e.g. data:image/;base64,. We recommend use only jpeg for photo images and png for graphic art.
  • brand_color: if not empty, set up brand color

Destroy organization

destroy_organization:(organization_id:<long>)
    -> { organization: <organization_record> }

with parameters:

  • organization_id: Organization ID

Change organization appearance

update_organization(organization_id: <long>, name: <opt_string>, icon: <opt_string>, brand_color: <opt_string>, allow_forwarding: <opt_boolean>)
    -> { organization: <organization_record> }
  • organization_id: Organization ID
  • name: if not empty, the organization name will be changed.
  • icon: if not empty, the organization icon will be reset. Icon data should be a complete image data url, e.g. data:image/;base64,. We recommend use only jpeg for photo images and png for graphic art.
  • brand_color: if not empty, set up brand color
  • allow_forwarding: Allow or prohibit message forwarding, default: false

List of organization users

get_organization_users(organization_id:<long>)
    -> { organizations_users: [<organizations_user_record>,...] }
  • organization_id: Organization ID

List of user organizations

get_organizations()
    -> { organizations: [<organization_record>, ...] }

Invite to the organization

Only the administrator has execute rights.

organization_invite(organization_id:<long>,user_id:<long>) 
    -> {organizations_user: <organizations_user_record>}

where

  • organization_id: Organization ID
  • user_id: ID of the user who is invited to the organization

on success, returns the user_organization_record of newly added user, send a notification of the new user in organization.

When a user is added to an organization, he automatically becomes a subscriber to all public chats of organizations.

Change user role

Only the administrator has execute rights.

Unless other user is a organization owner, admin can change other users roles, also promoting them to admin.

organization_access(organization_id:<long>, user_id:<long>, role:<string)
    ->{organizations_user: <organizations_user_record> }
  • organization_id: Organization ID
  • user_id: ID of the user to whom the rights are assigned
  • role: Valid values admin and ro

Owner access can not be changed.

Kick a user out of the organization.

Only the administrator has execute rights.

organization_kick(organization_id:<long>,user_id:<long>)
    -> {}
  • organization_id: Organization ID
  • user_id: ID of the user to whom the rights are assigned

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

The user will exit all chats of the organization.

Leave the organization

organization_leave(organization_id:<long>) -> {}

On successful, the notification of a deleted object is propagated among the group subscribers:

{
    event:'deleted', 
    object_name:'organization',
    object: { id: <organization_id_record> }
}

The user will exit all chats.

Create organization room

Only the administrator has execute rights.

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_organization_room(name: <string>, user_ids: [<int>,..], organization_id: <int>, type: <opt_string>)
    ->{subscription: <unichat subscription record> }
  • organization_id: Organization ID
  • name: name: required room name. Any string.
  • user_ids: optional array of userid to be invited immediately.
  • type: Optional. Valid values public and private, default: public

List of organization subscriptions

get_organization_subscriptions(organization_id: <int>, short: <booleab>, limit: <int>, offset: <int>)
    ->{subscriptions: [<unichat subscription record>,...] }
  • organization_id: Organization ID

  • 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.

For more information, see unichat subscription record for the record structure, and unichat groups and unichat subscriptions for overall explanation.

Tasks

In organizations, you can create tasks. Tasks can only belong to yourself, or to any group. A task is assigned to one performer, they may also have comments and attachments. When creating a task and specifying the executor, a chat is created with this user and this task is attached to this chat. If the task sets the group and the executor, then the executor must be in this group. Anyone who belongs to the group to which the task belongs can make changes.

Create task

create_task(organization_id:<opt_long>, title:<string>, description:<opt_string>, group_id:<opt_long>, executor_id:<opt_long>, uploads_id:<opt_long>)
    -> { task: <task_record> }

with parameters:

  • organization_id: organization ID
  • title: task title
  • description: task description
  • group_id: If present, then attached to the group
  • executor_id: If present, then it is attached to the group and if the group_id is specified, then the user must be in this group
  • uploads_id: are identifiers of attachments made through the attachment_by_chunks method. If the string is empty, all attachments will be destroyed. Available: nil, empty string, array, number

Update task

update_task(task_id:<integer>, title:<opt_string>, description:<opt_string>, group_id:<opt_integer>, executor_id:<opt_integer>, uploads_id:<opt_integer>, organization_id: <opt_long>, done: <opt_boolean>)
    -> { task: <task_record> }

with parameters:

  • task_id: task ID
  • title: task title
  • description: task description
  • group_id: If present, then attached to the group. If the group was previously specified, then you cannot change it.
  • executor_id: If present, then it is attached to the group and if the group_id is specified, then the user must be in this group
  • uploads_id: are identifiers of attachments made through the attachment_by_chunks method. If the string is empty, all attachments will be destroyed. Available: nil, empty string, array, number
  • organization_id: organization ID
  • done: if TRUE, then the task is completed.

Destroy task attachment

To destroy an attachment, you can call the method:

destroy_task_attachment(task_id:<long>, signed_id:<string>)
    -> {<task_record>}

with parameters:

Destroy task

Destroy task with all comments and attachments.

destroy_task(task_id:<long>)
    -> {}

with parameters:

  • task_id: task ID

List of organization tasks

organization_tasks(organization_id:<long>)
    -> { tasks: [<task_record>,...] }
  • organization_id: Organization ID

List of group tasks

group_tasks(subscription_id:<long>)
    -> { tasks: [<task_record>,...] }
  • subscription_id: Subscription ID

List of user tasks

user_tasks(organization_id:<opt_long>, user_id:<opt_long>)
    -> { tasks: [<task_record>,...] }
  • organization_id: Organization ID; if specified, used within this organization
  • user_id: User ID; if not specified, the current user is used

Create sub-task

A task may contain subtasks in the form of a checklist with statuses done/undone.

create_subtask(task_id:<long>, text:<string>)
    -> { subtask: <subtask_record> }

with parameters:

  • task_id: task ID
  • text: sub-task title

Update sub-task

A task may contain subtasks in the form of a list.

update_subtask(subtask_id:<long>, text:<string>, done:<opt_boolean>)
    -> { subtask: <subtask_record> }

with parameters:

  • subtask_id: task ID
  • text: sub-task title
  • done: status of sub-task (true/false)

Destroy sub-task

A task may contain subtasks in the form of a list.

delete_subtask(subtask_id:<long>)
    -> {}

with parameters:

  • subtask_id: sub-task ID

Sort order of sub-tasks

subtasks_ordering(task_id:<long>, subtasks:<long>)
    -> { task: <task_record> }

with parameters:

  • task_id: task ID
  • subtasks: array of identifiers of sub-tasks, example: [6, 3, 5, 4, 1, 2]

Add comment

create_task_comment(task_id:<long>, text:<string>)
    -> { task_comment: <task_comment_record> }

with parameters:

  • task_id: task ID
  • text: sub-task title

Load comments

task_comments(task_id:<long>, limit:<opt_integer>, offset:<opt_integer>, before_id:<opt_long>) 
    -> { comments: [<task_comment_record>,...] }

with parameters:

  • task_id: task ID
  • limit and offset allow paging in usual sense. Default limit is 50, default offset is 0.
  • before_id if present, select comments that are older than a given id, in most recent first.

Destroy comment

A task may contain subtasks in the form of a list.

delete_task_comment(comment_id:<long>)
    -> {}

with parameters:

  • comment_id: comment ID