// Squawk messenger. // Copyright (C) 2019 Yury Gubich // // 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 3 of the License, or // (at your option) any later version. // // 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 "application.h" Application::Application(Core::Squawk* p_core): QObject(), availability(Shared::Availability::offline), core(p_core), squawk(nullptr), notifications("org.freedesktop.Notifications", "/org/freedesktop/Notifications", "org.freedesktop.Notifications"), roster(), conversations(), dialogueQueue(roster), nowQuitting(false), destroyingSquawk(false), storage() { connect(&roster, &Models::Roster::unnoticedMessage, this, &Application::notify); connect(&roster, &Models::Roster::unreadMessagesCountChanged, this, &Application::unreadMessagesCountChanged); //connecting myself to the backed connect(this, &Application::changeState, core, &Core::Squawk::changeState); connect(this, &Application::setRoomJoined, core, &Core::Squawk::setRoomJoined); connect(this, &Application::setRoomAutoJoin, core, &Core::Squawk::setRoomAutoJoin); connect(this, &Application::subscribeContact, core, &Core::Squawk::subscribeContact); connect(this, &Application::unsubscribeContact, core, &Core::Squawk::unsubscribeContact); connect(this, &Application::replaceMessage, core, &Core::Squawk::replaceMessage); connect(this, &Application::sendMessage, core, &Core::Squawk::sendMessage); connect(this, &Application::resendMessage, core, &Core::Squawk::resendMessage); connect(&roster, &Models::Roster::requestArchive, std::bind(&Core::Squawk::requestArchive, core, std::placeholders::_1, std::placeholders::_2, 20, std::placeholders::_3)); connect(&dialogueQueue, &DialogQueue::modifyAccountRequest, core, &Core::Squawk::modifyAccountRequest); connect(&dialogueQueue, &DialogQueue::responsePassword, core, &Core::Squawk::responsePassword); connect(&dialogueQueue, &DialogQueue::disconnectAccount, core, &Core::Squawk::disconnectAccount); connect(&roster, &Models::Roster::fileDownloadRequest, core, &Core::Squawk::fileDownloadRequest); connect(&roster, &Models::Roster::localPathInvalid, core, &Core::Squawk::onLocalPathInvalid); //coonecting backend to myself connect(core, &Core::Squawk::stateChanged, this, &Application::stateChanged); connect(core, &Core::Squawk::accountMessage, &roster, &Models::Roster::addMessage); connect(core, &Core::Squawk::responseArchive, &roster, &Models::Roster::responseArchive); connect(core, &Core::Squawk::changeMessage, &roster, &Models::Roster::changeMessage); connect(core, &Core::Squawk::newAccount, &roster, &Models::Roster::addAccount); connect(core, &Core::Squawk::changeAccount, this, &Application::changeAccount); connect(core, &Core::Squawk::removeAccount, this, &Application::removeAccount); connect(core, &Core::Squawk::addContact, this, &Application::addContact); connect(core, &Core::Squawk::addGroup, this, &Application::addGroup); connect(core, &Core::Squawk::removeGroup, &roster, &Models::Roster::removeGroup); connect(core, qOverload(&Core::Squawk::removeContact), &roster, qOverload(&Models::Roster::removeContact)); connect(core, qOverload(&Core::Squawk::removeContact), &roster, qOverload(&Models::Roster::removeContact)); connect(core, &Core::Squawk::changeContact, &roster, &Models::Roster::changeContact); connect(core, &Core::Squawk::addPresence, &roster, &Models::Roster::addPresence); connect(core, &Core::Squawk::removePresence, &roster, &Models::Roster::removePresence); connect(core, &Core::Squawk::addRoom, &roster, &Models::Roster::addRoom); connect(core, &Core::Squawk::changeRoom, &roster, &Models::Roster::changeRoom); connect(core, &Core::Squawk::removeRoom, &roster, &Models::Roster::removeRoom); connect(core, &Core::Squawk::addRoomParticipant, &roster, &Models::Roster::addRoomParticipant); connect(core, &Core::Squawk::changeRoomParticipant, &roster, &Models::Roster::changeRoomParticipant); connect(core, &Core::Squawk::removeRoomParticipant, &roster, &Models::Roster::removeRoomParticipant); connect(core, &Core::Squawk::fileDownloadComplete, std::bind(&Models::Roster::fileComplete, &roster, std::placeholders::_1, false)); connect(core, &Core::Squawk::fileUploadComplete, std::bind(&Models::Roster::fileComplete, &roster, std::placeholders::_1, true)); connect(core, &Core::Squawk::fileProgress, &roster, &Models::Roster::fileProgress); connect(core, &Core::Squawk::fileError, &roster, &Models::Roster::fileError); connect(core, &Core::Squawk::requestPassword, this, &Application::requestPassword); connect(core, &Core::Squawk::ready, this, &Application::readSettings); QDBusConnection sys = QDBusConnection::sessionBus(); sys.connect( "org.freedesktop.Notifications", "/org/freedesktop/Notifications", "org.freedesktop.Notifications", "NotificationClosed", this, SLOT(onNotificationClosed(quint32, quint32)) ); sys.connect( "org.freedesktop.Notifications", "/org/freedesktop/Notifications", "org.freedesktop.Notifications", "ActionInvoked", this, SLOT(onNotificationInvoked(quint32, const QString&)) ); } Application::~Application() {} void Application::quit() { if (!nowQuitting) { nowQuitting = true; emit quitting(); writeSettings(); unreadMessagesCountChanged(0); //this notification persist in the desktop, for now I'll zero it on quit not to confuse people for (Conversations::const_iterator itr = conversations.begin(), end = conversations.end(); itr != end; ++itr) { disconnect(itr->second, &Conversation::destroyed, this, &Application::onConversationClosed); itr->second->close(); } conversations.clear(); dialogueQueue.quit(); if (squawk != nullptr) { squawk->close(); } if (!destroyingSquawk) { checkForTheLastWindow(); } } } void Application::checkForTheLastWindow() { if (QApplication::topLevelWidgets().size() > 0) { emit readyToQuit(); } else { connect(qApp, &QApplication::lastWindowClosed, this, &Application::readyToQuit); } } void Application::createMainWindow() { if (squawk == nullptr) { squawk = new Squawk(roster); connect(squawk, &Squawk::notify, this, &Application::notify); connect(squawk, &Squawk::changeSubscription, this, &Application::changeSubscription); connect(squawk, &Squawk::openedConversation, this, &Application::onSquawkOpenedConversation); connect(squawk, &Squawk::openConversation, this, &Application::openConversation); connect(squawk, &Squawk::changeState, this, &Application::setState); connect(squawk, &Squawk::closing, this, &Application::onSquawkClosing); connect(squawk, &Squawk::modifyAccountRequest, core, &Core::Squawk::modifyAccountRequest); connect(squawk, &Squawk::newAccountRequest, core, &Core::Squawk::newAccountRequest); connect(squawk, &Squawk::removeAccountRequest, core, &Core::Squawk::removeAccountRequest); connect(squawk, &Squawk::connectAccount, core, &Core::Squawk::connectAccount); connect(squawk, &Squawk::disconnectAccount, core, &Core::Squawk::disconnectAccount); connect(squawk, &Squawk::addContactRequest, core, &Core::Squawk::addContactRequest); connect(squawk, &Squawk::removeContactRequest, core, &Core::Squawk::removeContactRequest); connect(squawk, &Squawk::removeRoomRequest, core, &Core::Squawk::removeRoomRequest); connect(squawk, &Squawk::addRoomRequest, core, &Core::Squawk::addRoomRequest); connect(squawk, &Squawk::addContactToGroupRequest, core, &Core::Squawk::addContactToGroupRequest); connect(squawk, &Squawk::removeContactFromGroupRequest, core, &Core::Squawk::removeContactFromGroupRequest); connect(squawk, &Squawk::renameContactRequest, core, &Core::Squawk::renameContactRequest); connect(squawk, &Squawk::requestVCard, core, &Core::Squawk::requestVCard); connect(squawk, &Squawk::uploadVCard, core, &Core::Squawk::uploadVCard); connect(squawk, &Squawk::changeDownloadsPath, core, &Core::Squawk::changeDownloadsPath); connect(core, &Core::Squawk::responseVCard, squawk, &Squawk::responseVCard); dialogueQueue.setParentWidnow(squawk); squawk->stateChanged(availability); squawk->show(); } } void Application::onSquawkClosing() { dialogueQueue.setParentWidnow(nullptr); if (!nowQuitting) { disconnect(core, &Core::Squawk::responseVCard, squawk, &Squawk::responseVCard); } destroyingSquawk = true; squawk->deleteLater(); squawk = nullptr; //for now quit(); } void Application::onSquawkDestroyed() { destroyingSquawk = false; if (nowQuitting) { checkForTheLastWindow(); } } void Application::notify(const QString& account, const Shared::Message& msg) { QString jid = msg.getPenPalJid(); QString name = QString(roster.getContactName(account, jid)); QString path = QString(roster.getContactIconPath(account, jid, msg.getPenPalResource())); QVariantList args; args << QString(); uint32_t notificationId = qHash(msg.getId()); args << notificationId; if (path.size() > 0) { args << path; } else { args << QString("mail-message"); //TODO should here better be unknown user icon? } if (msg.getType() == Shared::Message::groupChat) { args << msg.getFromResource() + tr(" from ") + name; } else { args << name; } QString body(msg.getBody()); QString oob(msg.getOutOfBandUrl()); if (body == oob) { body = tr("Attached file"); } args << body; args << QStringList({ "markAsRead", tr("Mark as Read"), "openConversation", tr("Open conversation") }); args << QVariantMap({ {"desktop-entry", qApp->desktopFileName()}, {"category", QString("message")}, {"urgency", 1}, // {"sound-file", "/path/to/macaw/squawk"}, {"sound-name", QString("message-new-instant")} }); args << -1; notifications.callWithArgumentList(QDBus::AutoDetect, "Notify", args); storage.insert(std::make_pair(notificationId, std::make_pair(Models::Roster::ElId(account, name), msg.getId()))); if (squawk != nullptr) { QApplication::alert(squawk); } } void Application::onNotificationClosed(quint32 id, quint32 reason) { Notifications::const_iterator itr = storage.find(id); if (itr != storage.end()) { if (reason == 2) { //dissmissed by user (https://specifications.freedesktop.org/notification-spec/latest/ar01s09.html) //TODO may ba also mark as read? } if (reason != 1) { //just expired, can be activated again from history, so no removing for now storage.erase(id); qDebug() << "Notification" << id << "was closed"; } } } void Application::onNotificationInvoked(quint32 id, const QString& action) { qDebug() << "Notification" << id << action << "request"; Notifications::const_iterator itr = storage.find(id); if (itr != storage.end()) { if (action == "markAsRead") { roster.markMessageAsRead(itr->second.first, itr->second.second); } else if (action == "openConversation") { focusConversation(itr->second.first, "", itr->second.second); } } } void Application::unreadMessagesCountChanged(int count) { QDBusMessage signal = QDBusMessage::createSignal("/", "com.canonical.Unity.LauncherEntry", "Update"); signal << qApp->desktopFileName() + QLatin1String(".desktop"); signal << QVariantMap ({ {"count-visible", count != 0}, {"count", count} }); QDBusConnection::sessionBus().send(signal); } void Application::focusConversation(const Models::Roster::ElId& id, const QString& resource, const QString& messageId) { if (squawk != nullptr) { if (squawk->currentConversationId() != id) { QModelIndex index = roster.getContactIndex(id.account, id.name, resource); squawk->select(index); } if (squawk->isMinimized()) { squawk->showNormal(); } else { squawk->show(); } squawk->raise(); squawk->activateWindow(); } else { openConversation(id, resource); } //TODO focus messageId; } void Application::setState(Shared::Availability p_availability) { if (availability != p_availability) { availability = p_availability; emit changeState(availability); } } void Application::stateChanged(Shared::Availability state) { availability = state; if (squawk != nullptr) { squawk->stateChanged(state); } } void Application::readSettings() { QSettings settings; settings.beginGroup("ui"); int avail; if (settings.contains("availability")) { avail = settings.value("availability").toInt(); } else { avail = static_cast(Shared::Availability::online); } settings.endGroup(); setState(Shared::Global::fromInt(avail)); createMainWindow(); } void Application::writeSettings() { QSettings settings; settings.setValue("availability", static_cast(availability)); } void Application::requestPassword(const QString& account, bool authenticationError) { if (authenticationError) { dialogueQueue.addAction(account, DialogQueue::askCredentials); } else { dialogueQueue.addAction(account, DialogQueue::askPassword); } } void Application::onConversationClosed() { Conversation* conv = static_cast(sender()); Models::Roster::ElId id(conv->getAccount(), conv->getJid()); Conversations::const_iterator itr = conversations.find(id); if (itr != conversations.end()) { conversations.erase(itr); } if (conv->isMuc) { Room* room = static_cast(conv); if (!room->autoJoined()) { emit setRoomJoined(id.account, id.name, false); } } } void Application::changeSubscription(const Models::Roster::ElId& id, bool subscribe) { Models::Item::Type type = roster.getContactType(id); switch (type) { case Models::Item::contact: if (subscribe) { emit subscribeContact(id.account, id.name, ""); } else { emit unsubscribeContact(id.account, id.name, ""); } break; case Models::Item::room: setRoomAutoJoin(id.account, id.name, subscribe); if (!isConverstationOpened(id)) { emit setRoomJoined(id.account, id.name, subscribe); } break; default: break; } } void Application::subscribeConversation(Conversation* conv) { connect(conv, &Conversation::destroyed, this, &Application::onConversationClosed); connect(conv, &Conversation::sendMessage, this, &Application::onConversationMessage); connect(conv, &Conversation::replaceMessage, this, &Application::onConversationReplaceMessage); connect(conv, &Conversation::resendMessage, this, &Application::onConversationResend); connect(conv, &Conversation::notifyableMessage, this, &Application::notify); } void Application::openConversation(const Models::Roster::ElId& id, const QString& resource) { Conversations::const_iterator itr = conversations.find(id); Models::Account* acc = roster.getAccount(id.account); Conversation* conv = nullptr; bool created = false; if (itr != conversations.end()) { conv = itr->second; } else { Models::Element* el = roster.getElement(id); if (el != nullptr) { if (el->type == Models::Item::room) { created = true; Models::Room* room = static_cast(el); conv = new Room(acc, room); if (!room->getJoined()) { emit setRoomJoined(id.account, id.name, true); } } else if (el->type == Models::Item::contact) { created = true; conv = new Chat(acc, static_cast(el)); } } } if (conv != nullptr) { if (created) { conv->setAttribute(Qt::WA_DeleteOnClose); subscribeConversation(conv); conversations.insert(std::make_pair(id, conv)); } conv->show(); conv->raise(); conv->activateWindow(); if (resource.size() > 0) { conv->setPalResource(resource); } } } void Application::onConversationMessage(const Shared::Message& msg) { Conversation* conv = static_cast(sender()); QString acc = conv->getAccount(); roster.addMessage(acc, msg); emit sendMessage(acc, msg); } void Application::onConversationReplaceMessage(const QString& originalId, const Shared::Message& msg) { Conversation* conv = static_cast(sender()); QString acc = conv->getAccount(); roster.changeMessage(acc, msg.getPenPalJid(), originalId, { {"state", static_cast(Shared::Message::State::pending)} }); emit replaceMessage(acc, originalId, msg); } void Application::onConversationResend(const QString& id) { Conversation* conv = static_cast(sender()); QString acc = conv->getAccount(); QString jid = conv->getJid(); emit resendMessage(acc, jid, id); } void Application::onSquawkOpenedConversation() { subscribeConversation(squawk->currentConversation); Models::Roster::ElId id = squawk->currentConversationId(); const Models::Element* el = roster.getElementConst(id); if (el != nullptr && el->isRoom() && !static_cast(el)->getJoined()) { emit setRoomJoined(id.account, id.name, true); } } void Application::removeAccount(const QString& account) { Conversations::const_iterator itr = conversations.begin(); while (itr != conversations.end()) { if (itr->first.account == account) { Conversations::const_iterator lItr = itr; ++itr; Conversation* conv = lItr->second; disconnect(conv, &Conversation::destroyed, this, &Application::onConversationClosed); conv->close(); conversations.erase(lItr); } else { ++itr; } } if (squawk != nullptr && squawk->currentConversationId().account == account) { squawk->closeCurrentConversation(); } roster.removeAccount(account); } void Application::changeAccount(const QString& account, const QMap& data) { for (QMap::const_iterator itr = data.begin(), end = data.end(); itr != end; ++itr) { QString attr = itr.key(); roster.updateAccount(account, attr, *itr); } } void Application::addContact(const QString& account, const QString& jid, const QString& group, const QMap& data) { roster.addContact(account, jid, group, data); if (squawk != nullptr) { QSettings settings; settings.beginGroup("ui"); settings.beginGroup("roster"); settings.beginGroup(account); if (settings.value("expanded", false).toBool()) { QModelIndex ind = roster.getAccountIndex(account); squawk->expand(ind); } settings.endGroup(); settings.endGroup(); settings.endGroup(); } } void Application::addGroup(const QString& account, const QString& name) { roster.addGroup(account, name); if (squawk != nullptr) { QSettings settings; settings.beginGroup("ui"); settings.beginGroup("roster"); settings.beginGroup(account); if (settings.value("expanded", false).toBool()) { QModelIndex ind = roster.getAccountIndex(account); squawk->expand(ind); if (settings.value(name + "/expanded", false).toBool()) { squawk->expand(roster.getGroupIndex(account, name)); } } settings.endGroup(); settings.endGroup(); settings.endGroup(); } } bool Application::isConverstationOpened(const Models::Roster::ElId& id) const { return (conversations.count(id) > 0) || (squawk != nullptr && squawk->currentConversationId() == id);}