Implementation of work with Long Poll server in the VKontakte client for Sailfish OS

Introduction


Unfortunately, even now, in the modern world, it is not always possible to take advantage of all the benefits of push technology and sometimes you have to implement workarounds, for example, in the form of Long Poll, which allows you to emulate a push-notification mechanism. In particular, such a need arose in the implementation of the client VKontakte for Sailfish OS .

This article will not discuss the principles of interaction with the Long Poll VKontakte server - it has very detailed documentation , and the basic examples have already been published earlier . Instead, practical implementation for a specific platform will be considered.

It is understood that the reader is familiar with the development under Sailfish OS not only in QML , but also in C ++ .

Long poll client


The main client class is the LongPoll class, which queries the Long Poll server and parses its responses.

The getLongPollServer method, whose task is to obtain information for opening a connection to the server, is called during application initialization, which allows you to immediately receive custom updates:

 /** *       Long Poll  . */ void LongPoll::getLongPollServer() { QUrl url("https://api.vk.com/method/messages.getLongPollServer"); //    API QUrlQuery query; query.addQueryItem("access_token", _accessToken); //  Access Token query.addQueryItem("v", "5.53"); //    API url.setQuery(query); //       _manager->get(QNetworkRequest(url)); //  GET-    } 

In case of successful execution of the request, the connection information with the Long Poll server is saved and the connection is opened using the doLongPollRequest method:

 /* *      . * @:param: reply --    . */ void LongPoll::finished(QNetworkReply* reply) { QJsonDocument jDoc = QJsonDocument::fromJson(reply->readAll()); //    JSON if (_server.isNull() || _server.isEmpty()) { //      QJsonObject jObj = jDoc.object().value("response").toObject(); _server = jObj.value("server").toString(); //    _key = jObj.value("key").toString(); //    _ts = jObj.value("ts").toInt(); //     doLongPollRequest(); //    Long Poll  } else { // ... //     // ... } reply->deleteLater(); //     } 

In the doLongPollRequest Long Poll method, the server passes the necessary connection parameters:

 /* *     Long Poll . */ void LongPoll::doLongPollRequest() { QUrl url("https://" + _server); //    QUrlQuery query; query.addQueryItem("act", "a_check"); //     query.addQueryItem("key", _key); //   query.addQueryItem("ts", QString("%1").arg(_ts)); //    query.addQueryItem("wait", "25"); //  25   query.addQueryItem("mode", "10"); //       url.setQuery(query); //       _manager->get(QNetworkRequest(url)); //  GET-  Long Poll  } 

It is worth noting that the value of the mode field, equal to 10, was obtained by adding the option of receiving attachments (2) and returning the extended set of events (8).

In response to opening a connection, the server returns JSON containing the latest events. The answer is processed in the method finished :

 /* *      . * @:param: reply --    . */ void LongPoll::finished(QNetworkReply* reply) { QJsonDocument jDoc = QJsonDocument::fromJson(reply->readAll()); //    JSON if (_server.isNull() || _server.isEmpty()) { // ... //    // ... } else { QJsonObject jObj = jDoc.object(); if (jObj.contains("failed")) { //       if (jObj.value("failed").toInt() == 1) { //    _ts = jObj.value("ts").toInt(); //      doLongPollRequest(); //    Long Poll  } else { _server.clear(); //    _key.clear(); //    _ts = 0; //     getLongPollServer(); //      } } else { //      _ts = jObj.value("ts").toInt(); //      parseLongPollUpdates(jObj.value("updates").toArray()); //     doLongPollRequest(); //    Long Poll  } } reply->deleteLater(); //     } 

The failed field in the response can take four values, but only one of them, equal to one, does not require re-requesting information to connect to the Long Poll server. For this reason, the condition was added to the code

 jObj.value("failed").toInt() == 1 

The parseLongPollUpdates method is a simple loop for all incoming events with a check of their type:

 enum LONGPOLL_EVENTS { NEW_MESSAGE = 4, //   INPUT_MESSAGES_READ = 6, //    OUTPUT_MESSAGES_READ = 7, //    USER_TYPES_IN_DIALOG = 61, //      USER_TYPES_IN_CHAT = 62, //      UNREAD_DIALOGS_CHANGED = 80, //     }; /* *   ,   Long Poll . * @:param: updates --    . */ void LongPoll::parseLongPollUpdates(const QJsonArray& updates) { for (auto value : updates) { //     QJsonArray update = value.toArray(); //    switch (update.at(0).toInt()) { //    case NEW_MESSAGE: emit gotNewMessage(update.at(1).toInt()); break; case INPUT_MESSAGES_READ: emit readMessages(update.at(1).toInt(), update.at(2).toInt(), false); break; case OUTPUT_MESSAGES_READ: emit readMessages(update.at(1).toInt(), update.at(2).toInt(), true); break; case USER_TYPES_IN_DIALOG: emit userTyping(update.at(1).toInt(), 0); break; case USER_TYPES_IN_CHAT: emit userTyping(update.at(1).toInt(), update.at(2).toInt()); break; case UNREAD_DIALOGS_CHANGED: emit unreadDialogsCounterUpdated(update.at(1).toInt()); break; default: break; } } } 

From the code, it is clear that for each Long Poll event a signal is sent by the client, which must be processed by another part of the application. The signal argument is not the entire event object, but only the necessary parts of it. For example, the gotNewMessage signal transmits only the identifier of a new message for which its full content is requested :

 void VkSDK::_gotNewMessage(int id) { _messages->getById(id); } 

As a result of the execution of this one-line function , a request is sent to the VKontakte server to obtain complete information about the message by its identifier with the further creation of the object of this message. Finally, a signal is sent that is associated with the user interface, transmitting data about the new message, which is displayed in the notification panel:

 import QtQuick 2.0 //      QML import Sailfish.Silica 1.0 //      Sailfish OS import org.nemomobile.notifications 1.0 //      ApplicationWindow //   { // ... //   // ... Notification { //     id: commonNotification //    category: "harbour-kat" //   remoteActions: [ //  -    { "name": "default", "service": "nothing", "path": "nothing", "iface": "nothing", "method": "nothing" } ] } Connections { //     target: vksdk //    SDK  onGotNewMessage: { //      commonNotification.summary = name //      commonNotification.previewSummary = name //     commonNotification.body = preview //      commonNotification.previewBody = preview //     commonNotification.close() //      commonNotification.publish() //    } } } 

Dialog Interface


Now, based on the principles of client interaction with the Long Poll server and the principles of transferring the received information to the user interface, we can consider an example of updating the open dialog .

The first thing that catches your eye is the Connections component:

 Connections { //     target: vksdk //    SDK  onSavedPhoto: { //       attachmentsList += name + ","; //       attachmentsBusy.running = false; //     } onUserTyping: { //       var tempId = userId; //      if (chatId !== 0) { //    tempId = chatId; //  ,     } if (tempId === historyId) { //       typingLabel.visible = true //      } } } 

The onUserTyping slot handles the dialing event of the message by displaying the corresponding notification to the user. Here, in the first step, a room identifier is received (a room is a generic term for conversations and chats), and in the second, a notification is displayed if the received identifier and the identifier of the current room match.

It is worth noting that the notification of a set of messages is displayed for ten seconds, if during this time a new event did not arrive, a newly activated notification. This is provided by the Timer component:

 Label { //     id: typingLabel //    anchors.bottom: newmessagerow.top //       width: parent.width //     horizontalAlignment: Text.AlignHCenter //      font.pixelSize: Theme.fontSizeExtraSmall //    color: Theme.secondaryColor //     text: qsTr("typing...") //    visible: false //      onVisibleChanged: if (visible) typingLabelTimer.running = true //     Timer { //    id: typingLabelTimer //    interval: 10000 //   --   onTriggered: typingLabel.visible = false //       } } 

The onSavedPhoto slot onSavedPhoto responsible for handling the end of image loading in messages, which is beyond the scope of the current article.

The second thing that causes interest is the list of messages:

 SilicaListView { //    id: messagesListView //    //        : anchors.left: parent.left anchors.right: parent.right //            : anchors.top: parent.top anchors.bottom: typingLabel.top verticalLayoutDirection: ListView.BottomToTop //      clip: true //   ,     model: vksdk.messagesModel //    delegate: MessageItem { //     //        : anchors.left: parent.left anchors.right: parent.right //     : userId: fromId //   date: datetime //    out_: out //    read_: read //    avatarSource: avatar //    bodyText: body //   photos: photosList //     videos: videosList //     audios: audiosList //     documents: documentsList //     links: linksList //     news: newsList //       geoTile: geoTileUrl //     geoMap: geoMapUrl //         fwdMessages: fwdMessagesList //    Component.onCompleted: { //      if (index === vksdk.messagesModel.size-1) { //       //      : vksdk.messages.getHistory(historyId, vksdk.messagesModel.size) } } } VerticalScrollDecorator {} //      } 

Here, the MessageItem component is responsible for displaying a single message. His consideration is beyond the scope of this article.

The messages themselves are taken from the model vksdk.messagesModel . This model is a list of Message objects that can be updated in real time using the add , prepend , addProfile , readMessages and clear methods:

 /* *    . */ void MessagesModel::clear() { beginRemoveRows(QModelIndex(), 0, _messages.size()); //     _messages.clear(); //   _profiles.clear(); //    endRemoveRows(); //     //    : QModelIndex index = createIndex(0, 0, nullptr); emit dataChanged(index, index); } /* *      . * @:param: message --    . */ void MessagesModel::add(Message* message) { //   : beginInsertRows(QModelIndex(), _messages.size(), _messages.size()); _messages.append(message); //    endInsertRows(); //    //    : QModelIndex index = createIndex(0, 0, static_cast<void *>(0)); emit dataChanged(index, index); } /* *       . * @:param: message --    . */ void MessagesModel::prepend(Message* message) { //        : if (_messages.isEmpty()) return; if (message->chat() && _messages.at(0)->chatId() != message->chatId()) return; if (!message->chat() && _messages.at(0)->userId() != message->userId()) return; beginInsertRows(QModelIndex(), 0, 0); //    _messages.insert(0, message); //   endInsertRows(); //    //    : QModelIndex index = createIndex(0, _messages.size(), nullptr); emit dataChanged(index, index); } /* *       . * @:param: profile --    . */ void MessagesModel::addProfile(Friend* profile) { //   ,       if (_profiles.contains(profile->id())) return; _profiles[profile->id()] = profile; //    : QModelIndex startIndex = createIndex(0, 0, nullptr); QModelIndex endIndex = createIndex(_messages.size(), 0, nullptr); emit dataChanged(startIndex, endIndex); } /* *    . * @:param: peerId --  . * @:param: localId --    . * @:param: out --     . */ void MessagesModel::readMessages(qint64 peerId, qint64 localId, bool out) { //        : if (_messages.isEmpty()) return; if (_messages.at(0)->chat() && _messages.at(0)->chatId() != peerId) return; if (!_messages.at(0)->chat() && _messages.at(0)->userId() != peerId) return; foreach (Message *message, _messages) { //       if (message->id() <= localId && message->isOut() == out) //    message->setReadState(true); //    } //    : QModelIndex startIndex = createIndex(0, 0, nullptr); QModelIndex endIndex = createIndex(_messages.size(), 0, nullptr); emit dataChanged(startIndex, endIndex); } 

A common dataChanged all five methods is the use of the dataChanged signal, which indicates that the model has been updated. Emitting this signal updates the SilicaListView elements to display the current status of the messages. Adding messages to the SilicaListView accomplished by calling the beginInsertRows and endInsertRows that send the rowsAboutToBeInserted and rowsInserted respectively. As a result, the user will see in the dialogue new messages and their status in real time.

Conclusion


This article examined the interaction with the Long Poll server when developing for Sailfish OS using the example of the VKontakte application. Some features of the client implementation and how to update the user interface in real time were considered. The code for the application described in this article is available on GitHub.

Source: https://habr.com/ru/post/413947/


All Articles