Implementação do trabalho com o servidor Long Poll no cliente VKontakte para Sailfish OS

1. Introdução


Infelizmente, mesmo agora, no mundo moderno, nem sempre é possível aproveitar os benefícios da tecnologia push e, às vezes, é necessário implementar soluções alternativas, por exemplo, na forma de Long Poll, que permite emular o mecanismo de notificações push. Em particular, surgiu essa necessidade ao implementar o cliente VKontakte para o Sailfish OS .

Este artigo não discutirá os princípios de interação com o servidor Long Poll VKontakte - ele possui documentação muito detalhada e exemplos básicos já foram publicados anteriormente . Em vez disso, será considerada uma implementação prática para uma plataforma específica.

Entende-se que o leitor está familiarizado com o desenvolvimento do sistema operacional Sailfish, não apenas em QML , mas também em C ++ .

Cliente Long Poll


A classe principal do cliente é a classe LongPoll , que consulta o servidor Long Poll e analisa suas respostas.

O método getLongPollServer , cuja tarefa é obter informações para abrir uma conexão com o servidor, é chamado durante a inicialização do aplicativo, o que permite que você receba imediatamente atualizações do usuário:

 /** *       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-    } 

Se a solicitação for concluída com êxito, as informações de conexão com o servidor Long Poll doLongPollRequest salvas e a conexão será aberta usando o método doLongPollRequest :

 /* *      . * @: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(); //     } 

No método doLongPollRequest Long Poll, os parâmetros de conexão necessários são transmitidos ao servidor:

 /* *     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  } 

Vale ressaltar que o valor do campo mode , igual a 10, foi obtido adicionando a opção de receber anexos (2) e retornando um conjunto estendido de eventos (8).

Como resposta à abertura de uma conexão, o servidor retorna um JSON contendo os eventos mais recentes. A resposta é processada no método 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(); //     } 

O campo com failed na resposta pode assumir quatro valores, mas apenas um deles, igual a um, não requer uma solicitação repetida de informações para conectar-se ao servidor Long Poll. Por esse motivo, a condição foi adicionada ao código.

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

O método parseLongPollUpdates é um ciclo simples para todos os eventos recebidos com uma verificação do seu tipo:

 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; } } } 

O código mostra que, para cada novo evento Long Poll, o cliente envia um sinal que deve ser processado por outra parte do aplicativo. O argumento do sinal não é o objeto de evento inteiro, mas apenas as partes necessárias. Por exemplo, o sinal gotNewMessage transmite apenas o identificador de uma nova mensagem, pela qual seu conteúdo completo é solicitado :

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

Como resultado dessa função de linha única , é feita uma solicitação ao servidor VKontakte para obter informações completas sobre a mensagem pelo seu identificador, com a criação adicional do objeto dessa mensagem. Em conclusão, um sinal é enviado associado à interface do usuário, transmitindo dados sobre uma nova mensagem, que é exibida no painel de notificações:

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

Interface de diálogo


Agora, com base nos princípios de interação do cliente com o servidor Long Poll e nos princípios de transferência das informações recebidas para a interface do usuário, podemos considerar um exemplo de atualização de um diálogo aberto.

A primeira coisa que chama sua atenção é o componente Connections :

 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 //      } } } 

O slot onUserTyping processa o evento de um conjunto pelo interlocutor da mensagem, exibindo a notificação correspondente ao usuário. Aqui, na primeira etapa, é obtido o identificador da sala (a sala significa o termo generalizado para diálogos e bate-papos) e, no segundo estágio, uma notificação é exibida se o identificador recebido e o identificador da sala atual corresponderem.

É importante notar que uma notificação sobre um conjunto de mensagens é exibida por dez segundos, se durante esse período não chegar um novo evento que ativará a notificação novamente. Isso é obtido usando o componente Timer :

 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 //       } } 

O slot onSavedPhoto responsável pelo processamento do final do carregamento da imagem nas mensagens, que está além do escopo do artigo atual.

A segunda coisa que interessa é a lista de mensagens:

 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 {} //      } 

Aqui, o componente MessageItem é responsável por exibir uma única mensagem. Sua consideração está além do escopo deste artigo.

As próprias mensagens são obtidas do modelo vksdk.messagesModel . Este modelo é uma lista de objetos da Message que podem ser atualizados em tempo real usando os métodos add , addProfile , readMessages , readMessages e clear :

 /* *    . */ 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); } 

Comum a todos os cinco métodos é o uso do sinal dataChanged , que indica que os dados foram atualizados no modelo. A emissão desse sinal atualiza os elementos SilicaListView para exibir o status atual das mensagens. A adição de mensagens ao SilicaListView fornecida chamando os endInsertRows e endInsertRows que enviam os rowsInserted e rowsInserted respectivamente. Como resultado, o usuário verá novas mensagens e seu status em tempo real na caixa de diálogo.

Conclusão


Este artigo examinou a interação com o servidor Long Poll ao desenvolver para o Sailfish OS usando o aplicativo VK como exemplo. Alguns recursos da implementação do cliente e uma maneira de atualizar a interface do usuário em tempo real foram considerados. O código para o aplicativo descrito neste artigo está disponível no GitHub.

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


All Articles