diff --git a/src/ddpclient.cpp b/src/ddpclient.cpp index 5cdf2861..7c26d461 100644 --- a/src/ddpclient.cpp +++ b/src/ddpclient.cpp @@ -1,344 +1,368 @@ /* * * Copyright 2016 Riccardo Iaconelli * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License or (at your option) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * */ #include "ddpclient.h" #include #include #include #include #include #include "ruqola.h" void process_test(QJsonDocument doc) { qDebug() << "Callback test:" << doc; qDebug() << "End callback"; } void login_callback(QJsonDocument doc) { qDebug() << "LOGIN:" << doc; Ruqola::self()->setAuthToken(doc.object().value("token").toString()); qDebug() << "End callback"; } void DDPClient::resume_login_callback(QJsonDocument doc) { qDebug() << "LOGIN:" << doc; Ruqola::self()->setAuthToken(doc.object().value("token").toString()); qDebug() << "End callback"; } void empty_callback(QJsonDocument doc) { Q_UNUSED(doc); } DDPClient::DDPClient(const QString& url, QObject* parent) : QObject(parent), m_url(url), m_uid(1), m_loginJob(0), m_loginStatus(NotConnected), m_connected(false), m_attemptedPasswordLogin(false), m_attemptedTokenLogin(false) { m_webSocket.ignoreSslErrors(); connect(&m_webSocket, &QWebSocket::connected, this, &DDPClient::onWSConnected); connect(&m_webSocket, &QWebSocket::textMessageReceived, this, &DDPClient::onTextMessageReceived); connect(&m_webSocket, &QWebSocket::disconnected, this, &DDPClient::WSclosed); connect(Ruqola::self(), &Ruqola::serverURLChanged, this, &DDPClient::onServerURLChange); if (!url.isEmpty()) { m_webSocket.open(QUrl("wss://"+url+"/websocket")); } qDebug() << "Trying to connect to URL" << url; } DDPClient::~DDPClient() { m_webSocket.close(); } void DDPClient::onServerURLChange() { if (Ruqola::self()->serverURL() != m_url || !m_webSocket.isValid()) { if (m_webSocket.isValid()) { m_webSocket.flush(); m_webSocket.close(); } m_url = Ruqola::self()->serverURL(); m_webSocket.open(QUrl("wss://"+m_url+"/websocket")); connect(&m_webSocket, &QWebSocket::connected, this, &DDPClient::onWSConnected); qDebug() << "Reconnecting" << m_url; //<< m_webSocket.st; } } DDPClient::LoginStatus DDPClient::loginStatus() const { return m_loginStatus; } bool DDPClient::isConnected() const { return m_connected; } bool DDPClient::isLoggedIn() const { return m_loginStatus == LoggedIn; } +bool unsentMessages(){ + if ( !DDPClient::m_messageQueue.empty() ){ + QPair pair = DDPClient::m_messageQueue.head(); + int id = pair.first; + QJsonDocument params = pair.second; + if (DDPClient::loginStatus() == DDPClient::LoggedIn){ + DDPClient::method("sendMessage", params); + } + + //if it is sent successfully, dequeue it + //else it'll stay at head in queue for sending again + QHash::iterator it = DDPClient::m_messageStatus.find(id); + if ( it!= DDPClient::m_messageStatus.end() ){ + if ( it.value() == true ) + DDPClient::m_messageQueue.dequeue(); + } + } +} + unsigned int DDPClient::method(const QString& m, const QJsonDocument& params) { return method(m, params, empty_callback); } unsigned int DDPClient::method(const QString& method, const QJsonDocument& params, std::function callback) { QJsonObject json; json["msg"] = "method"; json["method"] = method; json["id"] = QString::number(m_uid); if (params.isArray()){ json["params"] = params.array(); } else if (params.isObject()) { QJsonArray arr; arr.append(params.object()); json["params"] = arr; -// params.object(); } qint64 bytes = m_webSocket.sendTextMessage(QJsonDocument(json).toJson(QJsonDocument::Compact)); if (bytes < json.length()) { qDebug() << "ERROR! I couldn't send all of my message. This is a bug! (try again)"; qDebug() << m_webSocket.isValid() << m_webSocket.error() << m_webSocket.requestUrl(); + + //try sending the message again + DDPClient::m_messageQueue.enqueue(qMakePair(m_uid-1, params)); + DDPClient::m_messageStatus.insert(m_uid-1,false); + } else { qDebug() << "Successfully sent " << json; + DDPClient::m_messageStatus.insert(m_uid-1,true); } //callback(QJsonDocument::fromJson(json.toUtf8())); m_callbackHash[m_uid] = callback; m_uid++; return m_uid - 1 ; } void DDPClient::subscribe(const QString& collection, const QJsonArray& params) { QJsonObject json; json["msg"] = "sub"; json["id"] = QString::number(m_uid); json["name"] = collection; json["params"] = params; qint64 bytes = m_webSocket.sendTextMessage(QJsonDocument(json).toJson(QJsonDocument::Compact)); if (bytes < json.length()) { qDebug() << "ERROR! I couldn't send all of my message. This is a bug! (try again)"; } m_uid++; } void DDPClient::onTextMessageReceived(QString message) { QJsonDocument response = QJsonDocument::fromJson(message.toUtf8()); if (!response.isNull() && response.isObject()) { QJsonObject root = response.object(); QString messageType = root.value("msg").toString(); qDebug() << "--------------------"; qDebug() << "--------------------"; // qDebug() << "Root is- " << root; if (messageType == "updated") { } else if (messageType == "result") { unsigned id = root.value("id").toString().toInt(); if (m_callbackHash.contains(id)) { std::function callback = m_callbackHash.take(id); QJsonDocument res = QJsonDocument(root.value("result").toObject()); QJsonObject result = res.object(); QString type = result.value("type").toString(); QString msg = result.value("msg").toString(); QByteArray base64Image; QImage image; QString path = QStandardPaths::writableLocation(QStandardPaths::CacheLocation); QDir dir(path); if (!dir.exists()){ dir.mkdir(path); qDebug() << "Directory created at " << path; } QDir::setCurrent(path); const QDateTime currentTime = QDateTime::currentDateTime(); const QString timestamp = currentTime.toString(QLatin1String("yyyyMMdd-hhmmsszzz")); const QString filename = QString::fromLatin1("%1.jpg").arg(timestamp); if (type == "image"){ qDebug() << "I am here yay"; base64Image.append(msg); image.loadFromData(QByteArray::fromBase64(base64Image), "JPG"); if ( !image.isNull() ){ qDebug() << "Saving Image to " << path; if (image.save(filename, "JPEG") ){ qDebug() << "Image saved successfully"; } else { qDebug() << "Image NOT saved"; } } else{ qDebug() << "Image is NULL"; } } else if (type == "text"){ } callback( QJsonDocument(root.value("result").toObject()) ); } emit result(id, QJsonDocument(root.value("result").toObject())); if (id == m_loginJob) { if (root.value("error").toObject().value("error").toInt() == 403) { qDebug() << "Wrong password or token expired"; login(); // Let's keep trying to log in } else { Ruqola::self()->setAuthToken(root.value("result").toObject().value("token").toString()); setLoginStatus(DDPClient::LoggedIn); } // emit loggedInChanged(); } } else if (messageType == "connected") { qDebug() << "Connected"; m_connected = true; emit connectedChanged(); setLoginStatus(DDPClient::LoggingIn); login(); // Try to resume auth token login } else if (messageType == "error") { qDebug() << "ERROR!!" << message; } else if (messageType == "ping") { qDebug() << "Ping - Pong"; QJsonObject pong; pong["msg"] = "pong"; m_webSocket.sendBinaryMessage(QJsonDocument(pong).toJson(QJsonDocument::Compact)); } else if (messageType == "added"){ qDebug() << "ADDING" <password().isEmpty()) { // If we have a password and we couldn't log in, let's stop here if (m_attemptedPasswordLogin) { setLoginStatus(LoginFailed); return; } m_attemptedPasswordLogin = true; QJsonObject user; user["username"] = Ruqola::self()->userName(); QJsonObject json; json["password"] = Ruqola::self()->password(); json["user"] = user; m_loginJob = method("login", QJsonDocument(json)); } else if (!Ruqola::self()->authToken().isEmpty() && !m_attemptedTokenLogin) { m_attemptedPasswordLogin = true; QJsonObject json; json["resume"] = Ruqola::self()->authToken(); m_loginJob = method("login", QJsonDocument(json)); } else { setLoginStatus(LoginFailed); } } void DDPClient::logOut() { // setLoginStatus(NotConnected); m_webSocket.close(); } void DDPClient::onWSConnected() { qDebug() << "Websocket connected at URL" << m_url; QJsonArray supportedVersions; supportedVersions.append("1"); QJsonObject protocol; protocol["msg"] = "connect"; protocol["version"] = "1"; protocol["support"] = supportedVersions; // QString json("{\"msg\":\"connect\", \"version\": \"1\", \"support\": [\"1\"]}"); QByteArray serialize = QJsonDocument(protocol).toJson(QJsonDocument::Compact); qint64 bytes = m_webSocket.sendTextMessage(serialize); if (bytes < serialize.length()) { qDebug() << "ERROR! I couldn't send all of my message. This is a bug! (try again)"; } else { qDebug() << "Successfully sent " << serialize; } } void DDPClient::WSclosed() { qDebug() << "WebSocket CLOSED" << m_webSocket.closeReason() << m_webSocket.error() << m_webSocket.closeCode(); setLoginStatus(NotConnected); // m_connected = false; } diff --git a/src/ddpclient.h b/src/ddpclient.h index 2b818167..3d3e523c 100644 --- a/src/ddpclient.h +++ b/src/ddpclient.h @@ -1,129 +1,137 @@ /* * * Copyright 2016 Riccardo Iaconelli * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License or (at your option) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * */ #ifndef DDPCLIENT_H #define DDPCLIENT_H // #include // #include // #include #include #include #include class QJsonObject; class QJsonDocument; class QUrl; class QWebSocket; class DDPClient : public QObject { Q_OBJECT public: enum LoginStatus { NotConnected, LoggingIn, LoggedIn, LoginFailed, LoggedOut }; Q_ENUM(LoginStatus) - DDPClient(const QString &url = QString(), QObject *parent = 0); ~DDPClient(); /** * @brief Call a method with name @param method and parameters @param params * * @param method The name of the method * @param params The parameters * @return unsigned int, the ID of the called method. Watch for it */ unsigned method(const QString &method, const QJsonDocument ¶ms); unsigned method(const QString &method, const QJsonDocument ¶ms, std::function callback); // unsigned method(const QString &method, const QJsonObject ¶ms); void subscribe(const QString &collection, const QJsonArray ¶ms); Q_INVOKABLE void login(); void logOut(); // Q_INVOKABLE void loginWithPassword(); bool isConnected() const; bool isLoggedIn() const; void onServerURLChange(); + //Again try to send unsent message; returns true if message was sent successfully + bool unsentMessages(); + signals: // void connected(); void connectedChanged(); void loginStatusChanged(); // void loggedInChanged(); void disconnected(); /** * @brief Emitted whenever a result is received. The parameter is the expected ID. * * @param id the ID received in the method() call */ void result(unsigned id, QJsonDocument result); void added(QJsonObject item); void changed(QJsonObject item); private slots: void onWSConnected(); void onTextMessageReceived(QString message); void WSclosed(); private: LoginStatus loginStatus() const; void setLoginStatus(LoginStatus l); void resume_login_callback(QJsonDocument doc); QString m_url; QWebSocket m_webSocket; unsigned m_uid; QHash > m_callbackHash; unsigned m_loginJob; LoginStatus m_loginStatus; bool m_connected; bool m_attemptedPasswordLogin; bool m_attemptedTokenLogin; + //pair- int (m_uid), QJsonDocument (params) + QQueue> m_messageQueue; + + //message with m_uid sent succussfully or not + QHash m_messageStatus; + friend class Ruqola; }; // #include "ddpclient.moc" #endif // DDPCLIENT_H diff --git a/src/messagemodel.cpp b/src/messagemodel.cpp index 34c62a0c..aa512aec 100644 --- a/src/messagemodel.cpp +++ b/src/messagemodel.cpp @@ -1,236 +1,235 @@ /* * * Copyright 2016 Riccardo Iaconelli * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License or (at your option) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * */ #include #include #include #include #include // #include #include #include #include "messagemodel.h" #include "ruqola.h" Message MessageModel::fromJSon(const QJsonObject& o) { Message message; message.messageID = o["messageID"].toString(); message.roomID = o["roomID"].toString(); message.message = o["message"].toString(); message.timestamp = (qint64) o["timestamp"].toDouble(); message.username = o["username"].toString(); message.userID = o["userID"].toString(); message.updatedAt = (qint64) o["updatedAt"].toDouble(); message.editedAt = (qint64) o["editedAt"].toDouble(); message.editedByUsername = o["editedByUsername"].toString(); message.editedByUserID = o["editedByUserID"].toString(); message.url = o["url"].toString(); message.meta = o["meta"].toString(); message.headers = o["headers"].toString(); message.parsedUrl = o["parsedUrl"].toString(); - message.image_url = o["image_url"].toString(); + message.imageUrl = o["imageUrl"].toString(); message.color = o["color"].toString(); message.alias = o["alias"].toString(); message.avatar = o["avatar"].toString(); message.groupable = o["groupable"].toBool(); message.parseUrls = o["parseUrls"].toBool(); message.systemMessage = o["systemMessage"].toBool(); message.systemMessageType = o["type"].toString(); return message; } QByteArray MessageModel::serialize(const Message& message) { QJsonDocument d; QJsonObject o; o["messageID"] = message.messageID; o["roomID"] = message.roomID; o["message"] = message.message; o["timestamp"] = message.timestamp; o["username"] = message.username; o["userID"] = message.userID; o["updatedAt"] = message.updatedAt; o["editedAt"] = message.editedAt; o["editedByUsername"] = message.editedByUsername; o["editedByUserID"] = message.editedByUserID; o["url"] = message.url; o["meta"] = message.meta; o["headers"] = message.headers; o["parsedUrl"] = message.parsedUrl; - o["image_url"] = message.image_url; + o["imageUrl"] = message.imageUrl; o["color"] = message.color; o["alias"] = message.alias; o["avatar"] = message.avatar; o["groupable"] = message.groupable; o["parseUrls"] = message.parseUrls; o["systemMessage"] = message.systemMessage; o["type"] = message.systemMessageType; d.setObject(o); return d.toBinaryData(); } MessageModel::MessageModel(const QString &roomID, QObject* parent) : QAbstractListModel(parent), m_roomID(roomID) { qDebug() << "Creating message Model"; QDir cacheDir(Ruqola::self()->cacheBasePath()+"/rooms_cache"); // load cache if (QFile::exists(cacheDir.absoluteFilePath(roomID)) && !roomID.isEmpty()) { QFile f(cacheDir.absoluteFilePath(roomID)); if (f.open(QIODevice::ReadOnly)) { QDataStream in(&f); while (!f.atEnd()) { char * byteArray; quint32 length; in.readBytes(byteArray, length); QByteArray arr = QByteArray::fromRawData(byteArray, length); Message m = MessageModel::fromJSon(QJsonDocument::fromBinaryData(arr).object()); addMessage(m); // m_allMessages[m.timestamp] = m; // qDebug() << m.message; } } } } MessageModel::~MessageModel() { QDir cacheDir(Ruqola::self()->cacheBasePath()+"/rooms_cache"); qDebug() << "Caching to..." << cacheDir.path(); if (!cacheDir.exists(cacheDir.path())) { cacheDir.mkpath(cacheDir.path()); } QFile f(cacheDir.absoluteFilePath(m_roomID)); if (f.open(QIODevice::WriteOnly)) { QDataStream out(&f); foreach (const Message m, m_allMessages) { QByteArray ms = MessageModel::serialize(m); out.writeBytes(ms, ms.size()); } } } QHash MessageModel::roleNames() const { QHash roles; roles[MessageText] = "messageText"; roles[Username] = "username"; roles[Timestamp] = "timestamp"; roles[UserID] = "userID"; roles[SystemMessage] = "systemMessage"; roles[SystemMessageType] = "type"; return roles; } qint64 MessageModel::lastTimestamp() const { if (m_allMessages.size()) { qDebug() << "returning timestamp" << m_allMessages.last().timestamp; return m_allMessages.last().timestamp; } else { return 0; } } int MessageModel::rowCount(const QModelIndex& parent) const { // qDebug() << "C++ asked for rowcount " << m_allMessages.size(); // if (m_allMessages.contains(m_currentRoom)) { return m_allMessages.size(); (void)parent; } void MessageModel::addMessage(const Message& message) { // Don't add empty messages if (message.message.isEmpty()) { return; } auto existingMessage = qFind(m_allMessages.begin(), m_allMessages.end(), message); bool present = (existingMessage != m_allMessages.end()); - auto i = qUpperBound(m_allMessages.begin(), m_allMessages.end(), message); + auto i = std::upper_bound(m_allMessages.begin(), m_allMessages.end(), message); int pos = i-m_allMessages.begin(); bool messageChanged = false; // if (qFind(m_allMessages.begin(), m_allMessages.end(), message) != m_allMessages.end()) { if (present){ // if (pos != m_allMessages.size()) { // we're at the end // qDebug() << "detecting a message change"; messageChanged = true; //Figure out a better way to update just the really changed message } else { beginInsertRows(QModelIndex(), pos, pos); } if (messageChanged) { m_allMessages.replace(pos-1, message); } else { m_allMessages.insert(i, message); } if (messageChanged) { emit dataChanged(createIndex(1, 1), createIndex(pos, 1)); } else { endInsertRows(); } } QVariant MessageModel::data(const QModelIndex& index, int role) const { int idx = index.row();//-1; if (role == MessageModel::Username) { return m_allMessages.at(idx).username; } else if (role == MessageModel::MessageText) { return m_allMessages.at(idx).message; } else if (role == MessageModel::Timestamp) { return QVariant(m_allMessages.at(idx).timestamp); } else if (role == MessageModel::UserID) { return QVariant(m_allMessages.at(idx).userID); } else if (role == MessageModel::SystemMessage) { return QVariant(m_allMessages.at(idx).systemMessage); } else if (role == MessageModel::SystemMessageType) { return QVariant(m_allMessages.at(idx).systemMessageType); } else { return QVariant(""); } } - // #include "messagelist.moc" diff --git a/src/messagemodel.h b/src/messagemodel.h index a66366d9..4ac98f8d 100644 --- a/src/messagemodel.h +++ b/src/messagemodel.h @@ -1,158 +1,178 @@ /* * * Copyright 2016 Riccardo Iaconelli * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License or (at your option) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * */ #ifndef MESSAGEMODEL_H #define MESSAGEMODEL_H #include #include #include #include #include #include class Message { public: + + enum MessageStatus { + Unsent, + Sending, + Sent, + SendFailed + }; + Q_ENUM(MessageStatus) + // To be used in ID find: message ID inline bool operator==(const Message &other) const { return other.messageID == messageID; } // To be used in sorted insert: timestamp inline bool operator<(const Message &other) const { return timestamp < other.timestamp; } + bool isSent() const; + //Message Object Fields // _id QString messageID; // rid QString roomID; // msg QString message; // ts qint64 timestamp; // u QString username; QString userID; // _updatedAt qint64 updatedAt; // editedAt qint64 editedAt; // editedBy QString editedByUsername; QString editedByUserID; // urls QString url; QString meta; QString headers; QString parsedUrl; // attachments - QString image_url; + QString imageUrl; QString color; // alias QString alias; // avatar QString avatar; // groupable bool groupable; // parseUrls bool parseUrls; bool systemMessage = false; QString systemMessageType; +signals: + void MessageStatusChanged(); + +private: + + MessageStatus messageStatus() const; + void setMessageStatus(MessageStatus m); + + MessageStatus m_messageStatus; }; class MessageModel : public QAbstractListModel { Q_OBJECT public: enum MessageRoles { Username = Qt::UserRole + 1, MessageText, Timestamp, UserID, SystemMessage, SystemMessageType, MessageID, RoomID, UpdatedAt, EditedAt, EditedByUserName, EditedByUserID, Url, Meta, Headers, ParsedUrl, - Image_url, + ImageUrl, Color, Alias, Avatar, Groupable, ParseUrls }; MessageModel(const QString &roomID = "no_room", QObject *parent = 0); virtual ~MessageModel(); void addMessage(const Message& message); virtual int rowCount(const QModelIndex & parent = QModelIndex()) const; virtual QVariant data(const QModelIndex & index, int role = Qt::DisplayRole) const; qint64 lastTimestamp() const; static Message fromJSon(const QJsonObject &source); static QByteArray serialize(const Message &message); protected: virtual QHash roleNames() const; private: const QString m_roomID; QVector m_allMessages; // QMap m_allMessages; // QMap m_allMessages; QString m_writableLocation; QFile *cacheWriter; }; #endif