/*
* Copyright (c) 2010-2024 Belledonne Communications SARL.
*
* This file is part of linphone-desktop
* (see https://www.linphone.org).
*
* 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 "CoreModel.hpp"
#include
#include
#include
#include
#include
#include
#include
#include "core/App.hpp"
#include "core/notifier/Notifier.hpp"
#include "core/path/Paths.hpp"
#include "model/tool/ToolModel.hpp"
#include "tool/Utils.hpp"
#if defined(Q_OS_MACOS)
#include "core/event-count-notifier/EventCountNotifierMacOs.hpp"
#else
#include "core/event-count-notifier/EventCountNotifierSystemTrayIcon.hpp"
#endif // if defined(Q_OS_MACOS)
// =============================================================================
DEFINE_ABSTRACT_OBJECT(CoreModel)
std::shared_ptr CoreModel::gCoreModel;
CoreModel::CoreModel(const QString &configPath, QThread *parent)
: ::Listener(nullptr, parent) {
connect(parent, &QThread::finished, this, [this]() {
// Model thread
if (mCore && mCore->getGlobalState() == linphone::GlobalState::On) mCore->stop();
gCoreModel = nullptr;
});
mConfigPath = configPath;
mLogger = std::make_shared(this);
mLogger->init();
moveToThread(parent);
}
CoreModel::~CoreModel() {
}
std::shared_ptr CoreModel::create(const QString &configPath, QThread *parent) {
if (gCoreModel) return gCoreModel;
auto model = std::make_shared(configPath, parent);
model->setSelf(model);
gCoreModel = model;
return model;
}
void CoreModel::start() {
mIterateTimer = new QTimer(this);
mIterateTimer->setInterval(30);
connect(mIterateTimer, &QTimer::timeout, [this]() {
static int iterateCount = 0;
if (iterateCount != 0) lCritical() << log().arg("Multi Iterate ! ");
++iterateCount;
mCore->iterate();
--iterateCount;
});
setPathBeforeCreation();
mCore =
linphone::Factory::get()->createCore(Utils::appStringToCoreString(Paths::getConfigFilePath(mConfigPath)),
Utils::appStringToCoreString(Paths::getFactoryConfigFilePath()), nullptr);
setMonitor(mCore);
mCore->enableRecordAware(true);
mCore->setVideoDisplayFilter("MSQOGL");
mCore->usePreviewWindow(true);
// Force capture/display.
// Useful if the app was built without video support.
// (The capture/display attributes are reset by the core in this case.)
auto config = mCore->getConfig();
if (!mCore->videoSupported()) {
config->setInt("video", "capture", 0);
config->setInt("video", "display", 0);
}
// TODO : set the real transport type when sdk will be updated
// for now, we need to let the OS choose the port to listen on
// so that the user can be connected to linphone and another softphone
// at the same time (otherwise it tries to listen on the same port as
// the other software)
auto transports = mCore->getTransports();
transports->setTcpPort(-2);
transports->setUdpPort(-2);
transports->setTlsPort(-2);
mCore->setTransports(transports);
mCore->enableVideoPreview(false); // SDK doesn't write the state in configuration if not ready.
config->setInt("video", "show_local", 0); // So : write ourself to turn off camera before starting the core.
QString userAgent = ToolModel::computeUserAgent(config);
mCore->setUserAgent(Utils::appStringToCoreString(userAgent), LINPHONESDK_VERSION);
mCore->start();
migrate();
setPathAfterStart();
if (SettingsModel::clearLocalLdapFriendsUponStartup(config)) {
// Remove ldap friends cache list. If not, old stored friends will take priority on merge and will not be
// updated from new LDAP requests..
auto ldapFriendList = mCore->getFriendListByName("ldap_friends");
if (ldapFriendList) mCore->removeFriendList(ldapFriendList);
}
mCore->enableFriendListSubscription(true);
if (mCore->getLogCollectionUploadServerUrl().empty())
mCore->setLogCollectionUploadServerUrl(Constants::DefaultUploadLogsServer);
mIterateTimer->start();
auto linphoneSearch = mCore->createMagicSearch();
linphoneSearch->setLimitedSearch(true);
mMagicSearch = Utils::makeQObject_ptr(linphoneSearch);
mMagicSearch->setSelf(mMagicSearch);
connect(mMagicSearch.get(), &MagicSearchModel::searchResultsReceived, this,
[this] { emit magicSearchResultReceived(mMagicSearch->mLastSearch); });
mStarted = true;
}
// -----------------------------------------------------------------------------
bool CoreModel::isInitialized() const {
return mStarted;
}
std::shared_ptr CoreModel::getInstance() {
return gCoreModel;
}
std::shared_ptr CoreModel::getCore() {
mustBeInLinphoneThread(log().arg(Q_FUNC_INFO));
return mCore;
}
std::shared_ptr CoreModel::getLogger() {
mustBeInLinphoneThread(log().arg(Q_FUNC_INFO));
return mLogger;
}
//-------------------------------------------------------------------------------
void CoreModel::setConfigPath(QString path) {
if (mConfigPath != path) {
mConfigPath = path;
if (!mCore) {
qWarning() << log().arg("Setting config path after core creation is not yet supported");
}
}
}
//-------------------------------------------------------------------------------
// PATHS
//-------------------------------------------------------------------------------
#define SET_FACTORY_PATH(TYPE, PATH) \
do { \
lInfo() << QStringLiteral("[CoreModel] Set `%1` factory path: `%2`").arg(#TYPE).arg(PATH); \
factory->set##TYPE##Dir(Utils::appStringToCoreString(PATH)); \
} while (0);
void CoreModel::setPathBeforeCreation() {
std::shared_ptr factory = linphone::Factory::get();
SET_FACTORY_PATH(Msplugins, Paths::getPackageMsPluginsDirPath());
SET_FACTORY_PATH(TopResources, Paths::getPackageTopDirPath());
SET_FACTORY_PATH(SoundResources, Paths::getPackageSoundsResourcesDirPath());
SET_FACTORY_PATH(DataResources, Paths::getPackageDataDirPath());
SET_FACTORY_PATH(Data, Paths::getAppLocalDirPath());
SET_FACTORY_PATH(Download, Paths::getDownloadDirPath());
SET_FACTORY_PATH(Config, Paths::getConfigDirPath(true));
}
void CoreModel::setPathsAfterCreation() {
auto friendsPath = Paths::getFriendsListFilePath();
if (!friendsPath.isEmpty() && QFileInfo(friendsPath).exists()) {
lInfo() << log().arg("Using old friends database at %1").arg(friendsPath);
std::shared_ptr config = mCore->getConfig();
config->setString("storage", "friends_db_uri", Utils::appStringToCoreString(friendsPath));
}
}
void CoreModel::setPathAfterStart() {
// Use application path if Linphone default is not available
if (mCore->getZrtpSecretsFile().empty() ||
!Paths::filePathExists(Utils::coreStringToAppString(mCore->getZrtpSecretsFile()), true))
mCore->setZrtpSecretsFile(Utils::appStringToCoreString(Paths::getZrtpSecretsFilePath()));
lInfo() << "[CoreModel] Using ZrtpSecrets path : " << QString::fromStdString(mCore->getZrtpSecretsFile());
// Use application path if Linphone default is not available
if (mCore->getUserCertificatesPath().empty() ||
!Paths::filePathExists(Utils::coreStringToAppString(mCore->getUserCertificatesPath()), true))
mCore->setUserCertificatesPath(Utils::appStringToCoreString(Paths::getUserCertificatesDirPath()));
lInfo() << "[CoreModel] Using UserCertificate path : " << QString::fromStdString(mCore->getUserCertificatesPath());
// Use application path if Linphone default is not available
if (mCore->getRootCa().empty() || !Paths::filePathExists(Utils::coreStringToAppString(mCore->getRootCa())))
mCore->setRootCa(Utils::appStringToCoreString(Paths::getRootCaFilePath()));
lInfo() << "[CoreModel] Using RootCa path : " << QString::fromStdString(mCore->getRootCa());
}
//-------------------------------------------------------------------------------
// FETCH CONFIG
//-------------------------------------------------------------------------------
QString CoreModel::getFetchConfig(QString filePath, bool *error) {
*error = false;
if (!filePath.isEmpty()) {
if (QUrl(filePath).isRelative()) { // this is a file path
filePath = Paths::getConfigFilePath(filePath, false);
if (!filePath.isEmpty()) filePath = "file://" + filePath;
}
if (filePath.isEmpty()) {
qWarning() << "Remote provisionning cannot be retrieved. Command have been cleaned";
*error = true;
}
}
return filePath;
}
void CoreModel::useFetchConfig(QString filePath) {
bool error = false;
filePath = getFetchConfig(filePath, &error);
if (!error && !filePath.isEmpty()) {
if (mCore && mCore->getGlobalState() == linphone::GlobalState::On) {
// TODO
// if (mSettings->getAutoApplyProvisioningConfigUriHandlerEnabled()) setFetchConfig(filePath); else
emit requestFetchConfig(filePath);
} else {
connect(
this, &CoreModel::globalStateChanged, this, [filePath, this]() { useFetchConfig(filePath); },
Qt::SingleShotConnection);
}
}
}
bool CoreModel::setFetchConfig(QString filePath) {
bool fetched = false;
qDebug() << "setFetchConfig with " << filePath;
if (!filePath.isEmpty()) {
if (mCore) {
filePath.replace('\\', '/');
QUrl url(filePath);
fetched = mCore->setProvisioningUri(Utils::appStringToCoreString(url.toEncoded())) == 0;
}
}
if (!fetched) {
qWarning() << "Remote provisionning cannot be retrieved. Command have been cleaned";
} else emit requestRestart();
return fetched;
}
void CoreModel::migrate() {
std::shared_ptr config = mCore->getConfig();
int rcVersion = config->getInt(SettingsModel::UiSection, Constants::RcVersionName, 0);
if (rcVersion == Constants::RcVersionCurrent) return;
if (rcVersion > Constants::RcVersionCurrent) {
lWarning() << log()
.arg("RC file version (%1) is more recent than app rc file version (%2)!!!")
.arg(rcVersion)
.arg(Constants::RcVersionCurrent);
return;
}
lInfo() << log().arg("Migrate from old rc file (%1 to %2).").arg(rcVersion).arg(Constants::RcVersionCurrent);
bool setLimeServerUrl = false;
for (const auto &account : mCore->getAccountList()) {
auto params = account->getParams();
auto newParams = params->clone();
QString accountIdentity =
(newParams->getIdentityAddress() ? newParams->getIdentityAddress()->asString().c_str() : "no-identity");
if (params->getDomain() == Constants::LinphoneDomain) {
if (rcVersion < 1) {
newParams->setContactParameters(Constants::DefaultContactParameters);
newParams->setExpires(Constants::DefaultExpires);
lInfo() << log().arg("Migrating") << accountIdentity
<< "for version 1. contact parameters =" << Constants::DefaultContactParameters
<< ", expires =" << Constants::DefaultExpires;
}
if (rcVersion < 2) {
bool exists = newParams->getConferenceFactoryUri() != "";
setLimeServerUrl = true;
if (!exists) {
newParams->setConferenceFactoryAddress(ToolModel::interpretUrl(Constants::DefaultConferenceURI));
}
lInfo() << log().arg("Migrating") << accountIdentity << "for version 2. Conference factory URI"
<< (exists ? std::string("unchanged") : std::string("= ") + Constants::DefaultConferenceURI)
.c_str();
// note: using std::string.c_str() to avoid having double quotes in qInfo()
}
if (rcVersion < 3) {
newParams->enableCpimInBasicChatRoom(true);
lInfo() << log().arg("Migrating") << accountIdentity
<< "for version 3. Enable Cpim in basic chat rooms";
}
if (rcVersion < 4) {
newParams->enableRtpBundle(true);
lInfo() << log().arg("Migrating") << accountIdentity << "for version 4. Enable RTP bundle mode";
}
if (rcVersion < 5) {
bool exists = !!newParams->getAudioVideoConferenceFactoryAddress();
setLimeServerUrl = true;
if (!exists)
newParams->setAudioVideoConferenceFactoryAddress(
ToolModel::interpretUrl(Constants::DefaultVideoConferenceURI));
lInfo() << log().arg("Migrating") << accountIdentity << "for version 5. Video conference factory URI"
<< (exists ? std::string("unchanged")
: std::string("= ") + Constants::DefaultVideoConferenceURI)
.c_str();
// note: using std::string.c_str() to avoid having double quotes in qInfo()
}
if (rcVersion < 6) { // Last 5.2 (5.2.6)
newParams->setPublishExpires(Constants::DefaultPublishExpires);
lInfo() << log().arg("Migrating") << accountIdentity
<< "for version 6. publish expires =" << Constants::DefaultPublishExpires;
}
if (rcVersion < 7) { // First 6.x
// 6.x reg_route added to use/create-app-sip-account.rc files on 6.x
if (newParams->getRoutesAddresses().empty()) {
std::list> routes;
routes.push_back(ToolModel::interpretUrl(Constants::DefaultRouteAddress));
newParams->setRoutesAddresses(routes);
lInfo() << log().arg("Migrating") << accountIdentity
<< "for version 7. Setting route to: " << Constants::DefaultRouteAddress;
}
// File transfer server URL modified to use/create-app-sip-account.rc files on 6.x
if (mCore->getLogCollectionUploadServerUrl() == Constants::RetiredUploadLogsServer) {
mCore->setLogCollectionUploadServerUrl(Constants::DefaultUploadLogsServer);
lInfo() << log().arg("Migrating") << accountIdentity
<< "for version 7. Setting Log collection upload server rul to: "
<< Constants::DefaultUploadLogsServer;
}
}
}
if (rcVersion < 7) { // 6.x lime algo c25519 added to all 6.x rc files
newParams->setLimeAlgo("c25519");
lInfo() << log().arg("Migrating") << accountIdentity << "for version 7. lime algo = c25519";
}
account->setParams(newParams);
}
if (rcVersion < 7) { // 6.x
// Video policy added to all 6.x rc files - done via config as API calls only saves config for
// these when core is ready.
if (!config->hasEntry("video", "automatically_accept")) config->setInt("video", "automatically_accept", 1);
if (!config->hasEntry("video", "automatically_initiate")) config->setInt("video", "automatically_initiate", 0);
if (!config->hasEntry("video", "automatically_accept_direction"))
config->setInt("video", "automatically_accept_direction", 2);
lInfo() << log().arg("Migrating) Video Policy for version 7.");
}
config->setInt(SettingsModel::UiSection, Constants::RcVersionName, Constants::RcVersionCurrent);
}
void CoreModel::searchInMagicSearch(QString filter,
int sourceFlags,
LinphoneEnums::MagicSearchAggregation aggregation,
int maxResults) {
mMagicSearch->search(filter, sourceFlags, aggregation, maxResults);
}
//---------------------------------------------------------------------------------------------------------------------------
void CoreModel::onAccountAdded(const std::shared_ptr &core,
const std::shared_ptr &account) {
emit accountAdded(core, account);
}
void CoreModel::onAccountRemoved(const std::shared_ptr &core,
const std::shared_ptr &account) {
if (core->getDefaultAccount() == nullptr && core->getAccountList().size() > 0)
core->setDefaultAccount(core->getAccountList().front());
emit accountRemoved(core, account);
}
void CoreModel::onAccountRegistrationStateChanged(const std::shared_ptr &core,
const std::shared_ptr &account,
linphone::RegistrationState state,
const std::string &message) {
emit accountRegistrationStateChanged(core, account, state, message);
}
void CoreModel::onAuthenticationRequested(const std::shared_ptr &core,
const std::shared_ptr &authInfo,
linphone::AuthMethod method) {
if (method == linphone::AuthMethod::Bearer) {
auto serverUrl = Utils::coreStringToAppString(authInfo->getAuthorizationServer());
auto username = Utils::coreStringToAppString(authInfo->getUsername());
auto realm = Utils::coreStringToAppString(authInfo->getRealm());
if (!serverUrl.isEmpty()) {
qDebug() << "onAuthenticationRequested for Bearer. Initialize OpenID connection for " << username << "@"
<< realm << " at " << serverUrl;
QString key = username + '@' + realm + ' ' + serverUrl;
if (mOpenIdConnections.contains(key)) mOpenIdConnections[key]->deleteLater();
mOpenIdConnections[key] = new OIDCModel(authInfo, this);
}
}
emit authenticationRequested(core, authInfo, method);
}
void CoreModel::onCallEncryptionChanged(const std::shared_ptr &core,
const std::shared_ptr &call,
bool on,
const std::string &authenticationToken) {
emit callEncryptionChanged(core, call, on, authenticationToken);
}
void CoreModel::onCallLogUpdated(const std::shared_ptr &core,
const std::shared_ptr &callLog) {
if (callLog && callLog->getStatus() == linphone::Call::Status::Missed) emit unreadNotificationsChanged();
emit callLogUpdated(core, callLog);
}
void CoreModel::onCallStateChanged(const std::shared_ptr &core,
const std::shared_ptr &call,
linphone::Call::State state,
const std::string &message) {
if (state == linphone::Call::State::IncomingReceived) {
if (App::getInstance()->getNotifier()) App::getInstance()->getNotifier()->notifyReceivedCall(call);
if (!core->getConfig()->getBool(SettingsModel::UiSection, "disable_command_line", false) &&
!core->getConfig()->getString(SettingsModel::UiSection, "command_line", "").empty()) {
QString command = Utils::coreStringToAppString(
core->getConfig()->getString(SettingsModel::UiSection, "command_line", ""));
QString userName = Utils::coreStringToAppString(call->getRemoteAddress()->getUsername());
QString displayName =
call->getCallLog() && call->getCallLog()->getConferenceInfo()
? Utils::coreStringToAppString(call->getCallLog()->getConferenceInfo()->getSubject())
: Utils::coreStringToAppString(call->getRemoteAddress()->getDisplayName());
command = command.replace("$1", userName);
command = command.replace("$2", displayName);
Utils::runCommandLine(command);
}
}
if (state == linphone::Call::State::End && SettingsModel::dndEnabled(core->getConfig()) &&
core->getCallsNb() == 0) { // Disable tones in DND mode if no more calls are running.
SettingsModel::getInstance()->setCallToneIndicationsEnabled(false);
}
App::postModelAsync([core]() {
for (int i = 0; i < App::getInstance()->getAccountList()->rowCount(); ++i) {
auto accountCore = App::getInstance()->getAccountList()->getAt(i);
emit accountCore->lSetPresence(core->getCallsNb() == 0 ? LinphoneEnums::Presence::Online
: LinphoneEnums::Presence::Busy,
false, false);
}
});
emit callStateChanged(core, call, state, message);
}
void CoreModel::onCallStatsUpdated(const std::shared_ptr &core,
const std::shared_ptr &call,
const std::shared_ptr &stats) {
emit callStatsUpdated(core, call, stats);
}
void CoreModel::onCallCreated(const std::shared_ptr &lc, const std::shared_ptr &call) {
emit callCreated(call);
}
void CoreModel::onChatRoomRead(const std::shared_ptr &core,
const std::shared_ptr &chatRoom) {
emit chatRoomRead(core, chatRoom);
}
void CoreModel::onChatRoomStateChanged(const std::shared_ptr &core,
const std::shared_ptr &chatRoom,
linphone::ChatRoom::State state) {
emit chatRoomStateChanged(core, chatRoom, state);
}
void CoreModel::onConferenceInfoReceived(const std::shared_ptr &core,
const std::shared_ptr &conferenceInfo) {
emit conferenceInfoReceived(core, conferenceInfo);
}
void CoreModel::onConferenceStateChanged(const std::shared_ptr &core,
const std::shared_ptr &conference,
linphone::Conference::State state) {
emit conferenceStateChanged(core, conference, state);
}
void CoreModel::onConfiguringStatus(const std::shared_ptr &core,
linphone::ConfiguringState status,
const std::string &message) {
mConfigStatus = status;
mConfigMessage = Utils::coreStringToAppString(message);
emit configuringStatus(core, status, message);
}
void CoreModel::onDefaultAccountChanged(const std::shared_ptr &core,
const std::shared_ptr &account) {
emit defaultAccountChanged(core, account);
}
void CoreModel::onDtmfReceived(const std::shared_ptr &lc,
const std::shared_ptr &call,
int dtmf) {
emit dtmfReceived(lc, call, dtmf);
}
void CoreModel::onEcCalibrationResult(const std::shared_ptr &core,
linphone::EcCalibratorStatus status,
int delayMs) {
emit ecCalibrationResult(core, status, delayMs);
}
void CoreModel::onFirstCallStarted(const std::shared_ptr &core) {
emit firstCallStarted();
}
void CoreModel::onGlobalStateChanged(const std::shared_ptr &core,
linphone::GlobalState gstate,
const std::string &message) {
emit globalStateChanged(core, gstate, message);
}
void CoreModel::onIsComposingReceived(const std::shared_ptr &core,
const std::shared_ptr &room) {
emit isComposingReceived(core, room);
}
void CoreModel::onLastCallEnded(const std::shared_ptr &core) {
emit lastCallEnded();
}
void CoreModel::onLogCollectionUploadStateChanged(const std::shared_ptr &core,
linphone::Core::LogCollectionUploadState state,
const std::string &info) {
emit logCollectionUploadStateChanged(core, state, info);
}
void CoreModel::onLogCollectionUploadProgressIndication(const std::shared_ptr &lc,
size_t offset,
size_t total) {
emit logCollectionUploadProgressIndication(lc, offset, total);
}
void CoreModel::onMessageReceived(const std::shared_ptr &core,
const std::shared_ptr &room,
const std::shared_ptr &message) {
emit unreadNotificationsChanged();
std::list> messages;
messages.push_back(message);
if (App::getInstance()->getNotifier()) App::getInstance()->getNotifier()->notifyReceivedMessages(room, messages);
emit messageReceived(core, room, message);
}
void CoreModel::onMessagesReceived(const std::shared_ptr &core,
const std::shared_ptr &room,
const std::list> &messages) {
emit unreadNotificationsChanged();
if (App::getInstance()->getNotifier()) App::getInstance()->getNotifier()->notifyReceivedMessages(room, messages);
emit messagesReceived(core, room, messages);
}
void CoreModel::onNewMessageReaction(const std::shared_ptr &core,
const std::shared_ptr &chatRoom,
const std::shared_ptr &message,
const std::shared_ptr &reaction) {
emit newMessageReaction(core, chatRoom, message, reaction);
}
void CoreModel::onNotifyPresenceReceivedForUriOrTel(
const std::shared_ptr &core,
const std::shared_ptr &linphoneFriend,
const std::string &uriOrTel,
const std::shared_ptr &presenceModel) {
emit notifyPresenceReceivedForUriOrTel(core, linphoneFriend, uriOrTel, presenceModel);
}
void CoreModel::onNotifyPresenceReceived(const std::shared_ptr &core,
const std::shared_ptr &linphoneFriend) {
emit notifyPresenceReceived(core, linphoneFriend);
}
void CoreModel::onQrcodeFound(const std::shared_ptr &core, const std::string &result) {
emit qrcodeFound(core, result);
}
void CoreModel::onReactionRemoved(const std::shared_ptr &core,
const std::shared_ptr &chatRoom,
const std::shared_ptr &message,
const std::shared_ptr &address) {
emit reactionRemoved(core, chatRoom, message, address);
}
void CoreModel::onTransferStateChanged(const std::shared_ptr &core,
const std::shared_ptr &call,
linphone::Call::State state) {
emit transferStateChanged(core, call, state);
}
void CoreModel::onVersionUpdateCheckResultReceived(const std::shared_ptr &core,
linphone::VersionUpdateCheckResult result,
const std::string &version,
const std::string &url) {
emit versionUpdateCheckResultReceived(core, result, version, url);
}
void CoreModel::onFriendListRemoved(const std::shared_ptr &core,
const std::shared_ptr &friendList) {
// Hack because of SDK bug. Wait some times before removing friends.
// Note: shared pointers can be used with singleShot, they will be destroyed after removing lambda from timer.
QTimer::singleShot(500, [this, core, friendList]() {
emit friendListRemoved(core, friendList);
for (auto f : friendList->getFriends()) {
emit friendRemoved(f);
}
});
/* TODO when SDK bug is fixed
emit friendListRemoved(core, friendList);
qDebug() << "List removed: " << friendList->getDisplayName();
for (auto l : core->getFriendsLists()) {
qDebug() << "Still have " << l->getDisplayName();
}
for (auto f : friendList->getFriends()) {
auto linFriend = CoreModel::getInstance()->getCore()->findFriend(f->getAddress());
if (linFriend) qDebug() << "Friend still exist: " << linFriend->getFriendList()->getDisplayName();
emit friendRemoved(f);
}
*/
}