تنفيذ العمل مع خادم Long Poll في عميل VKontakte لنظام Sailfish OS

مقدمة


لسوء الحظ ، حتى الآن ، في العالم الحديث ، ليس من الممكن دائمًا الاستفادة من مزايا تقنية الدفع ، وأحيانًا يجب عليك تنفيذ الحلول ، على سبيل المثال ، في شكل استطلاع طويل ، والذي يسمح لك بمحاكاة آلية دفع الإشعارات. على وجه الخصوص ، نشأت هذه الحاجة عند تنفيذ عميل VKontakte لنظام Sailfish OS .

لن تناقش هذه المقالة مبادئ التفاعل مع خادم Long Poll VKontakte - تحتوي على وثائق مفصلة للغاية ، وقد تم بالفعل نشر أمثلة أساسية في وقت سابق . بدلاً من ذلك ، سيتم النظر في التنفيذ العملي لمنصة معينة.

من المفهوم أن القارئ على دراية بتطوير Sailfish OS ليس فقط في QML ، ولكن أيضًا في C ++ .

عميل استطلاع طويل


فئة العميل الرئيسية هي فئة LongPoll ، التي تستعلم عن خادم Long Poll وتقوم بتحليل استجاباتها.

يتم getLongPollServer طريقة getLongPollServer ، التي تتمثل مهمتها في الحصول على معلومات لفتح اتصال بالخادم ، أثناء تهيئة التطبيق ، مما يسمح لك بتلقي تحديثات المستخدم على الفور:

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

إذا تم إكمال الطلب بنجاح ، يتم حفظ معلومات الاتصال بخادم Long Poll ويتم فتح الاتصال باستخدام طريقة 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(); //     } 

في طريقة doLongPollRequest Long Poll ، يتم تمرير معلمات الاتصال الضرورية إلى الخادم:

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

تجدر الإشارة إلى أنه تم الحصول على قيمة حقل mode ، تساوي 10 ، عن طريق إضافة خيار استلام المرفقات (2) وإرجاع مجموعة موسعة من الأحداث (8).

كرد فعل لفتح اتصال ، يعرض الخادم JSON الذي يحتوي على أحدث الأحداث. تتم معالجة الاستجابة بالطريقة 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(); //     } 

يمكن أن يأخذ الحقل failed في الاستجابة أربع قيم ، ولكن واحدة فقط ، تساوي واحدة ، لا تتطلب طلبًا متكررًا للحصول على معلومات للاتصال بخادم الاستطلاع الطويل. لهذا السبب ، تمت إضافة الشرط إلى الرمز.

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

طريقة parseLongPollUpdates هي دورة بسيطة لجميع الأحداث الواردة مع التحقق من نوعها:

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

يُظهر الرمز أنه لكل حدث Long Poll جديد ، يرسل العميل إشارة يجب معالجتها بواسطة جزء آخر من التطبيق. الوسيطة إشارة ليست كائن الحدث بأكمله ، ولكن فقط الأجزاء الضرورية. على سبيل المثال ، gotNewMessage إشارة gotNewMessage فقط معرف الرسالة الجديدة ، والتي يتم من خلالها طلب محتواها الكامل:

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

نتيجة لهذه الوظيفة أحادية الخط ، يتم تقديم طلب إلى خادم فكونتاكتي للحصول على معلومات كاملة حول الرسالة من خلال معرفها مع إنشاء مزيد من كائن هذه الرسالة. في الختام ، يتم إرسال إشارة مرتبطة بواجهة المستخدم ، تنقل البيانات حول رسالة جديدة ، والتي يتم عرضها في لوحة الإخطار:

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

واجهة الحوار


الآن ، استنادًا إلى مبادئ تفاعل العميل مع خادم Long Poll ومبادئ نقل المعلومات المستلمة إلى واجهة المستخدم ، يمكننا التفكير في مثال لتحديث حوار مفتوح.

أول شيء يلفت انتباهك هو مكون 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 //      } } } 

تقوم فتحة onUserTyping بمعالجة حدث مجموعة بواسطة محاور الرسالة عن طريق عرض الإخطار المقابل للمستخدم. هنا ، في الخطوة الأولى ، يتم الحصول على معرف الغرفة (تعني الغرفة المصطلح العام للحوارات والدردشات) ، وفي المرحلة الثانية ، يتم عرض إشعار إذا كان المعرف المستلم ومعرف الغرفة الحالية متطابقين.

تجدر الإشارة إلى أنه يتم عرض إشعار حول مجموعة من الرسائل لمدة عشر ثوانٍ ، إذا لم يكن هناك خلال هذا الوقت حدث جديد ينشط الإخطار مرة أخرى. يتم تحقيق ذلك باستخدام مكون 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 //       } } 

تعتبر فتحة onSavedPhoto مسؤولة عن معالجة نهاية تحميل الصورة في الرسائل ، وهو ما يتجاوز نطاق المقالة الحالية.

الشيء الثاني الذي يهم هو قائمة الرسائل:

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

هنا ، يكون مكون MessageItem مسؤولاً عن عرض رسالة واحدة. نظرها خارج نطاق هذه المقالة.

يتم أخذ الرسائل نفسها من نموذج vksdk.messagesModel . هذا النموذج عبارة عن قائمة بكائنات Message التي يمكن تحديثها في الوقت الفعلي باستخدام addProfile ، readMessages ، readMessages ، readMessages والطرق 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); } 

من الشائع في جميع الطرق الخمسة استخدام إشارة dataChanged ، والتي تشير إلى أنه تم تحديث البيانات في النموذج. يؤدي تحديث هذه الإشارة إلى تحديث عناصر SilicaListView لعرض الحالة الحالية للرسائل. SilicaListView توفير إضافة الرسائل إلى SilicaListView عن طريق استدعاء endInsertRows و endInsertRows التي ترسل rowsInserted و rowsInserted على التوالي. ونتيجة لذلك ، سيرى المستخدم رسائل جديدة وحالتها في الوقت الفعلي في مربع الحوار.

الخلاصة


فحصت هذه المقالة التفاعل مع خادم Long Poll عند التطوير لنظام Sailfish OS باستخدام تطبيق VK كمثال. تم النظر في بعض ميزات تنفيذ العميل وطريقة لتحديث واجهة المستخدم في الوقت الحقيقي. يتوفر رمز التطبيق الموضح في هذه المقالة على GitHub.

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


All Articles