mirror of
https://github.com/qTox/qTox.git
synced 2024-03-22 14:00:36 +08:00
We we're calling toxav_* functions without synchronizing to any of the Tox threads. Additionally remove the call timeout, it creates timers from different threads, which causes errors. (cherry picked from commit 98cfe9838fb13cdc8d9dca346bd13a71056afc94)
1573 lines
47 KiB
C++
1573 lines
47 KiB
C++
/*
|
|
Copyright © 2013 by Maxim Biro <nurupo.contributions@gmail.com>
|
|
Copyright © 2014-2019 by The qTox Project Contributors
|
|
|
|
This file is part of qTox, a Qt-based graphical interface for Tox.
|
|
|
|
qTox is libre 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.
|
|
|
|
qTox 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 qTox. If not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
#include "core.h"
|
|
#include "corefile.h"
|
|
#include "src/core/coreav.h"
|
|
#include "src/core/dhtserver.h"
|
|
#include "src/core/icoresettings.h"
|
|
#include "src/core/toxlogger.h"
|
|
#include "src/core/toxoptions.h"
|
|
#include "src/core/toxstring.h"
|
|
#include "src/model/groupinvite.h"
|
|
#include "src/model/status.h"
|
|
#include "src/net/bootstrapnodeupdater.h"
|
|
#include "src/nexus.h"
|
|
#include "src/persistence/profile.h"
|
|
#include "src/util/strongtype.h"
|
|
|
|
#include <QCoreApplication>
|
|
#include <QRegularExpression>
|
|
#include <QString>
|
|
#include <QStringBuilder>
|
|
#include <QTimer>
|
|
|
|
#include <cassert>
|
|
#include <memory>
|
|
|
|
const QString Core::TOX_EXT = ".tox";
|
|
|
|
#define ASSERT_CORE_THREAD assert(QThread::currentThread() == coreThread.get())
|
|
|
|
namespace {
|
|
bool LogConferenceTitleError(TOX_ERR_CONFERENCE_TITLE error)
|
|
{
|
|
switch (error) {
|
|
case TOX_ERR_CONFERENCE_TITLE_OK:
|
|
break;
|
|
case TOX_ERR_CONFERENCE_TITLE_CONFERENCE_NOT_FOUND:
|
|
qWarning() << "Conference title not found";
|
|
break;
|
|
case TOX_ERR_CONFERENCE_TITLE_INVALID_LENGTH:
|
|
qWarning() << "Invalid conference title length";
|
|
break;
|
|
case TOX_ERR_CONFERENCE_TITLE_FAIL_SEND:
|
|
qWarning() << "Failed to send title packet";
|
|
}
|
|
return error;
|
|
}
|
|
|
|
bool parseToxErrBootstrap(Tox_Err_Bootstrap error)
|
|
{
|
|
switch(error) {
|
|
case TOX_ERR_BOOTSTRAP_OK:
|
|
return true;
|
|
|
|
case TOX_ERR_BOOTSTRAP_NULL:
|
|
qCritical() << "null argument when not expected";
|
|
return false;
|
|
|
|
case TOX_ERR_BOOTSTRAP_BAD_HOST:
|
|
qCritical() << "Could not resolve hostname, or invalid IP address";
|
|
return false;
|
|
|
|
case TOX_ERR_BOOTSTRAP_BAD_PORT:
|
|
qCritical() << "out of range port";
|
|
return false;
|
|
|
|
default:
|
|
qCritical() << "Unknown Tox_Err_bootstrap error code:" << error;
|
|
return false;
|
|
}
|
|
}
|
|
|
|
bool parseFriendSendMessageError(Tox_Err_Friend_Send_Message error)
|
|
{
|
|
switch (error) {
|
|
case TOX_ERR_FRIEND_SEND_MESSAGE_OK:
|
|
return true;
|
|
case TOX_ERR_FRIEND_SEND_MESSAGE_NULL:
|
|
qCritical() << "Send friend message passed an unexpected null argument";
|
|
return false;
|
|
case TOX_ERR_FRIEND_SEND_MESSAGE_FRIEND_NOT_FOUND:
|
|
qCritical() << "Send friend message could not find friend";
|
|
return false;
|
|
case TOX_ERR_FRIEND_SEND_MESSAGE_FRIEND_NOT_CONNECTED:
|
|
qCritical() << "Send friend message: friend is offline";
|
|
return false;
|
|
case TOX_ERR_FRIEND_SEND_MESSAGE_SENDQ:
|
|
qCritical() << "Failed to allocate more message queue";
|
|
return false;
|
|
case TOX_ERR_FRIEND_SEND_MESSAGE_TOO_LONG:
|
|
qCritical() << "Attemped to send message that's too long";
|
|
return false;
|
|
case TOX_ERR_FRIEND_SEND_MESSAGE_EMPTY:
|
|
qCritical() << "Attempted to send an empty message";
|
|
return false;
|
|
default:
|
|
qCritical() << "Unknown friend send message error:" << static_cast<int>(error);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
bool parseConferenceSendMessageError(Tox_Err_Conference_Send_Message error)
|
|
{
|
|
switch (error) {
|
|
case TOX_ERR_CONFERENCE_SEND_MESSAGE_OK:
|
|
return true;
|
|
|
|
case TOX_ERR_CONFERENCE_SEND_MESSAGE_CONFERENCE_NOT_FOUND:
|
|
qCritical() << "Conference not found";
|
|
return false;
|
|
|
|
case TOX_ERR_CONFERENCE_SEND_MESSAGE_FAIL_SEND:
|
|
qCritical() << "Conference message failed to send";
|
|
return false;
|
|
|
|
case TOX_ERR_CONFERENCE_SEND_MESSAGE_NO_CONNECTION:
|
|
qCritical() << "No connection";
|
|
return false;
|
|
|
|
case TOX_ERR_CONFERENCE_SEND_MESSAGE_TOO_LONG:
|
|
qCritical() << "Message too long";
|
|
return false;
|
|
default:
|
|
qCritical() << "Unknown Tox_Err_Conference_Send_Message error:" << static_cast<int>(error);
|
|
return false;
|
|
}
|
|
}
|
|
} // namespace
|
|
|
|
Core::Core(QThread* coreThread)
|
|
: tox(nullptr)
|
|
, av(nullptr)
|
|
, toxTimer{new QTimer{this}}
|
|
, coreThread(coreThread)
|
|
{
|
|
assert(toxTimer);
|
|
toxTimer->setSingleShot(true);
|
|
connect(toxTimer, &QTimer::timeout, this, &Core::process);
|
|
connect(coreThread, &QThread::finished, toxTimer, &QTimer::stop);
|
|
}
|
|
|
|
Core::~Core()
|
|
{
|
|
// need to reset av first, because it uses tox
|
|
av.reset();
|
|
|
|
coreThread->exit(0);
|
|
coreThread->wait();
|
|
|
|
tox.reset();
|
|
}
|
|
|
|
/**
|
|
* @brief Registers all toxcore callbacks
|
|
* @param tox Tox instance to register the callbacks on
|
|
*/
|
|
void Core::registerCallbacks(Tox* tox)
|
|
{
|
|
tox_callback_friend_request(tox, onFriendRequest);
|
|
tox_callback_friend_message(tox, onFriendMessage);
|
|
tox_callback_friend_name(tox, onFriendNameChange);
|
|
tox_callback_friend_typing(tox, onFriendTypingChange);
|
|
tox_callback_friend_status_message(tox, onStatusMessageChanged);
|
|
tox_callback_friend_status(tox, onUserStatusChanged);
|
|
tox_callback_friend_connection_status(tox, onConnectionStatusChanged);
|
|
tox_callback_friend_read_receipt(tox, onReadReceiptCallback);
|
|
tox_callback_conference_invite(tox, onGroupInvite);
|
|
tox_callback_conference_message(tox, onGroupMessage);
|
|
tox_callback_conference_peer_list_changed(tox, onGroupPeerListChange);
|
|
tox_callback_conference_peer_name(tox, onGroupPeerNameChange);
|
|
tox_callback_conference_title(tox, onGroupTitleChange);
|
|
}
|
|
|
|
/**
|
|
* @brief Factory method for the Core object
|
|
* @param savedata empty if new profile or saved data else
|
|
* @param settings Settings specific to Core
|
|
* @return nullptr or a Core object ready to start
|
|
*/
|
|
ToxCorePtr Core::makeToxCore(const QByteArray& savedata, const ICoreSettings* const settings,
|
|
ToxCoreErrors* err)
|
|
{
|
|
QThread* thread = new QThread();
|
|
if (thread == nullptr) {
|
|
qCritical() << "could not allocate Core thread";
|
|
return {};
|
|
}
|
|
thread->setObjectName("qTox Core");
|
|
|
|
auto toxOptions = ToxOptions::makeToxOptions(savedata, settings);
|
|
if (toxOptions == nullptr) {
|
|
qCritical() << "could not allocate Tox Options data structure";
|
|
if (err) {
|
|
*err = ToxCoreErrors::ERROR_ALLOC;
|
|
}
|
|
return {};
|
|
}
|
|
|
|
ToxCorePtr core(new Core(thread));
|
|
if (core == nullptr) {
|
|
if (err) {
|
|
*err = ToxCoreErrors::ERROR_ALLOC;
|
|
}
|
|
return {};
|
|
}
|
|
|
|
Tox_Err_New tox_err;
|
|
core->tox = ToxPtr(tox_new(*toxOptions, &tox_err));
|
|
|
|
switch (tox_err) {
|
|
case TOX_ERR_NEW_OK:
|
|
break;
|
|
|
|
case TOX_ERR_NEW_LOAD_BAD_FORMAT:
|
|
qCritical() << "failed to parse Tox save data";
|
|
if (err) {
|
|
*err = ToxCoreErrors::BAD_PROXY;
|
|
}
|
|
return {};
|
|
|
|
case TOX_ERR_NEW_PORT_ALLOC:
|
|
if (toxOptions->getIPv6Enabled()) {
|
|
toxOptions->setIPv6Enabled(false);
|
|
core->tox = ToxPtr(tox_new(*toxOptions, &tox_err));
|
|
if (tox_err == TOX_ERR_NEW_OK) {
|
|
qWarning() << "Core failed to start with IPv6, falling back to IPv4. LAN discovery "
|
|
"may not work properly.";
|
|
break;
|
|
}
|
|
}
|
|
|
|
qCritical() << "can't to bind the port";
|
|
if (err) {
|
|
*err = ToxCoreErrors::FAILED_TO_START;
|
|
}
|
|
return {};
|
|
|
|
case TOX_ERR_NEW_PROXY_BAD_HOST:
|
|
case TOX_ERR_NEW_PROXY_BAD_PORT:
|
|
case TOX_ERR_NEW_PROXY_BAD_TYPE:
|
|
qCritical() << "bad proxy, error code:" << tox_err;
|
|
if (err) {
|
|
*err = ToxCoreErrors::BAD_PROXY;
|
|
}
|
|
return {};
|
|
|
|
case TOX_ERR_NEW_PROXY_NOT_FOUND:
|
|
qCritical() << "proxy not found";
|
|
if (err) {
|
|
*err = ToxCoreErrors::BAD_PROXY;
|
|
}
|
|
return {};
|
|
|
|
case TOX_ERR_NEW_LOAD_ENCRYPTED:
|
|
qCritical() << "attempted to load encrypted Tox save data";
|
|
if (err) {
|
|
*err = ToxCoreErrors::INVALID_SAVE;
|
|
}
|
|
return {};
|
|
|
|
case TOX_ERR_NEW_MALLOC:
|
|
qCritical() << "memory allocation failed";
|
|
if (err) {
|
|
*err = ToxCoreErrors::ERROR_ALLOC;
|
|
}
|
|
return {};
|
|
|
|
case TOX_ERR_NEW_NULL:
|
|
qCritical() << "a parameter was null";
|
|
if (err) {
|
|
*err = ToxCoreErrors::FAILED_TO_START;
|
|
}
|
|
return {};
|
|
|
|
default:
|
|
qCritical() << "Tox core failed to start, unknown error code:" << tox_err;
|
|
if (err) {
|
|
*err = ToxCoreErrors::FAILED_TO_START;
|
|
}
|
|
return {};
|
|
}
|
|
|
|
// tox should be valid by now
|
|
assert(core->tox != nullptr);
|
|
|
|
// toxcore is successfully created, create toxav
|
|
// TODO(sudden6): don't create CoreAv here, Core should be usable without CoreAV
|
|
core->av = CoreAV::makeCoreAV(core->tox.get(), core->coreLoopLock);
|
|
if (!core->av) {
|
|
qCritical() << "Toxav failed to start";
|
|
if (err) {
|
|
*err = ToxCoreErrors::FAILED_TO_START;
|
|
}
|
|
return {};
|
|
}
|
|
|
|
// create CoreFile
|
|
core->file = CoreFile::makeCoreFile(core.get(), core->tox.get(), core->coreLoopLock);
|
|
if (!core->file) {
|
|
qCritical() << "CoreFile failed to start";
|
|
if (err) {
|
|
*err = ToxCoreErrors::FAILED_TO_START;
|
|
}
|
|
return {};
|
|
}
|
|
|
|
registerCallbacks(core->tox.get());
|
|
|
|
// connect the thread with the Core
|
|
connect(thread, &QThread::started, core.get(), &Core::onStarted);
|
|
core->moveToThread(thread);
|
|
|
|
// when leaving this function 'core' should be ready for it's start() action or
|
|
// a nullptr
|
|
return core;
|
|
}
|
|
|
|
void Core::onStarted()
|
|
{
|
|
ASSERT_CORE_THREAD;
|
|
|
|
// One time initialization stuff
|
|
QString name = getUsername();
|
|
if (!name.isEmpty()) {
|
|
emit usernameSet(name);
|
|
}
|
|
|
|
QString msg = getStatusMessage();
|
|
if (!msg.isEmpty()) {
|
|
emit statusMessageSet(msg);
|
|
}
|
|
|
|
ToxId id = getSelfId();
|
|
// Id comes from toxcore, must be valid
|
|
assert(id.isValid());
|
|
emit idSet(id);
|
|
|
|
loadFriends();
|
|
loadGroups();
|
|
|
|
process(); // starts its own timer
|
|
av->start();
|
|
emit avReady();
|
|
}
|
|
|
|
/**
|
|
* @brief Starts toxcore and it's event loop, can be called from any thread
|
|
*/
|
|
void Core::start()
|
|
{
|
|
coreThread->start();
|
|
}
|
|
|
|
|
|
/**
|
|
* @brief Returns the global widget's Core instance
|
|
*/
|
|
Core* Core::getInstance()
|
|
{
|
|
return Nexus::getCore();
|
|
}
|
|
|
|
const CoreAV* Core::getAv() const
|
|
{
|
|
return av.get();
|
|
}
|
|
|
|
CoreAV* Core::getAv()
|
|
{
|
|
return av.get();
|
|
}
|
|
|
|
CoreFile* Core::getCoreFile() const
|
|
{
|
|
return file.get();
|
|
}
|
|
|
|
/* Using the now commented out statements in checkConnection(), I watched how
|
|
* many ticks disconnects-after-initial-connect lasted. Out of roughly 15 trials,
|
|
* 5 disconnected; 4 were DCd for less than 20 ticks, while the 5th was ~50 ticks.
|
|
* So I set the tolerance here at 25, and initial DCs should be very rare now.
|
|
* This should be able to go to 50 or 100 without affecting legitimate disconnects'
|
|
* downtime, but lets be conservative for now. Edit: now ~~40~~ 30.
|
|
*/
|
|
#define CORE_DISCONNECT_TOLERANCE 30
|
|
|
|
/**
|
|
* @brief Processes toxcore events and ensure we stay connected, called by its own timer
|
|
*/
|
|
void Core::process()
|
|
{
|
|
QMutexLocker ml{&coreLoopLock};
|
|
|
|
ASSERT_CORE_THREAD;
|
|
|
|
static int tolerance = CORE_DISCONNECT_TOLERANCE;
|
|
tox_iterate(tox.get(), this);
|
|
|
|
#ifdef DEBUG
|
|
// we want to see the debug messages immediately
|
|
fflush(stdout);
|
|
#endif
|
|
|
|
// TODO(sudden6): recheck if this is still necessary
|
|
if (checkConnection()) {
|
|
tolerance = CORE_DISCONNECT_TOLERANCE;
|
|
} else if (!(--tolerance)) {
|
|
bootstrapDht();
|
|
tolerance = 3 * CORE_DISCONNECT_TOLERANCE;
|
|
}
|
|
|
|
unsigned sleeptime =
|
|
qMin(tox_iteration_interval(tox.get()), getCoreFile()->corefileIterationInterval());
|
|
toxTimer->start(sleeptime);
|
|
}
|
|
|
|
bool Core::checkConnection()
|
|
{
|
|
ASSERT_CORE_THREAD;
|
|
static bool isConnected = false;
|
|
bool toxConnected = tox_self_get_connection_status(tox.get()) != TOX_CONNECTION_NONE;
|
|
if (toxConnected && !isConnected) {
|
|
qDebug() << "Connected to the DHT";
|
|
emit connected();
|
|
} else if (!toxConnected && isConnected) {
|
|
qDebug() << "Disconnected from the DHT";
|
|
emit disconnected();
|
|
}
|
|
|
|
isConnected = toxConnected;
|
|
return toxConnected;
|
|
}
|
|
|
|
/**
|
|
* @brief Connects us to the Tox network
|
|
*/
|
|
void Core::bootstrapDht()
|
|
{
|
|
ASSERT_CORE_THREAD;
|
|
|
|
QList<DhtServer> bootstrapNodes = BootstrapNodeUpdater::loadDefaultBootstrapNodes();
|
|
|
|
int listSize = bootstrapNodes.size();
|
|
if (!listSize) {
|
|
qWarning() << "no bootstrap list?!?";
|
|
return;
|
|
}
|
|
|
|
int i = 0;
|
|
static int j = qrand() % listSize;
|
|
// i think the more we bootstrap, the more we jitter because the more we overwrite nodes
|
|
while (i < 2) {
|
|
const DhtServer& dhtServer = bootstrapNodes[j % listSize];
|
|
QString dhtServerAddress = dhtServer.address.toLatin1();
|
|
QString port = QString::number(dhtServer.port);
|
|
QString name = dhtServer.name;
|
|
qDebug() << QString("Connecting to a bootstrap node...");
|
|
QByteArray address = dhtServer.address.toLatin1();
|
|
// TODO: constucting the pk via ToxId is a workaround
|
|
ToxPk pk = ToxId{dhtServer.userId}.getPublicKey();
|
|
|
|
const uint8_t* pkPtr = pk.getData();
|
|
|
|
Tox_Err_Bootstrap error;
|
|
tox_bootstrap(tox.get(), address.constData(), dhtServer.port, pkPtr, &error);
|
|
parseToxErrBootstrap(error);
|
|
|
|
tox_add_tcp_relay(tox.get(), address.constData(), dhtServer.port, pkPtr, &error);
|
|
parseToxErrBootstrap(error);
|
|
|
|
++j;
|
|
++i;
|
|
}
|
|
}
|
|
|
|
void Core::onFriendRequest(Tox*, const uint8_t* cFriendPk, const uint8_t* cMessage,
|
|
size_t cMessageSize, void* core)
|
|
{
|
|
ToxPk friendPk(cFriendPk);
|
|
QString requestMessage = ToxString(cMessage, cMessageSize).getQString();
|
|
emit static_cast<Core*>(core)->friendRequestReceived(friendPk, requestMessage);
|
|
}
|
|
|
|
void Core::onFriendMessage(Tox*, uint32_t friendId, Tox_Message_Type type, const uint8_t* cMessage,
|
|
size_t cMessageSize, void* core)
|
|
{
|
|
bool isAction = (type == TOX_MESSAGE_TYPE_ACTION);
|
|
QString msg = ToxString(cMessage, cMessageSize).getQString();
|
|
emit static_cast<Core*>(core)->friendMessageReceived(friendId, msg, isAction);
|
|
}
|
|
|
|
void Core::onFriendNameChange(Tox*, uint32_t friendId, const uint8_t* cName, size_t cNameSize, void* core)
|
|
{
|
|
QString newName = ToxString(cName, cNameSize).getQString();
|
|
// no saveRequest, this callback is called on every connection, not just on name change
|
|
emit static_cast<Core*>(core)->friendUsernameChanged(friendId, newName);
|
|
}
|
|
|
|
void Core::onFriendTypingChange(Tox*, uint32_t friendId, bool isTyping, void* core)
|
|
{
|
|
emit static_cast<Core*>(core)->friendTypingChanged(friendId, isTyping);
|
|
}
|
|
|
|
void Core::onStatusMessageChanged(Tox*, uint32_t friendId, const uint8_t* cMessage,
|
|
size_t cMessageSize, void* core)
|
|
{
|
|
QString message = ToxString(cMessage, cMessageSize).getQString();
|
|
// no saveRequest, this callback is called on every connection, not just on name change
|
|
emit static_cast<Core*>(core)->friendStatusMessageChanged(friendId, message);
|
|
}
|
|
|
|
void Core::onUserStatusChanged(Tox*, uint32_t friendId, Tox_User_Status userstatus, void* core)
|
|
{
|
|
Status::Status status;
|
|
switch (userstatus) {
|
|
case TOX_USER_STATUS_AWAY:
|
|
status = Status::Status::Away;
|
|
break;
|
|
|
|
case TOX_USER_STATUS_BUSY:
|
|
status = Status::Status::Busy;
|
|
break;
|
|
|
|
default:
|
|
status = Status::Status::Online;
|
|
break;
|
|
}
|
|
|
|
// no saveRequest, this callback is called on every connection, not just on name change
|
|
emit static_cast<Core*>(core)->friendStatusChanged(friendId, status);
|
|
}
|
|
|
|
void Core::onConnectionStatusChanged(Tox*, uint32_t friendId, Tox_Connection status, void* vCore)
|
|
{
|
|
Core* core = static_cast<Core*>(vCore);
|
|
Status::Status friendStatus =
|
|
status != TOX_CONNECTION_NONE ? Status::Status::Online : Status::Status::Offline;
|
|
// Ignore Online because it will be emited from onUserStatusChanged
|
|
bool isOffline = friendStatus == Status::Status::Offline;
|
|
if (isOffline) {
|
|
emit core->friendStatusChanged(friendId, friendStatus);
|
|
core->checkLastOnline(friendId);
|
|
}
|
|
}
|
|
|
|
void Core::onGroupInvite(Tox* tox, uint32_t friendId, Tox_Conference_Type type,
|
|
const uint8_t* cookie, size_t length, void* vCore)
|
|
{
|
|
Core* core = static_cast<Core*>(vCore);
|
|
const QByteArray data(reinterpret_cast<const char*>(cookie), length);
|
|
const GroupInvite inviteInfo(friendId, type, data);
|
|
switch (type) {
|
|
case TOX_CONFERENCE_TYPE_TEXT:
|
|
qDebug() << QString("Text group invite by %1").arg(friendId);
|
|
emit core->groupInviteReceived(inviteInfo);
|
|
break;
|
|
|
|
case TOX_CONFERENCE_TYPE_AV:
|
|
qDebug() << QString("AV group invite by %1").arg(friendId);
|
|
emit core->groupInviteReceived(inviteInfo);
|
|
break;
|
|
|
|
default:
|
|
qWarning() << "Group invite with unknown type " << type;
|
|
}
|
|
}
|
|
|
|
void Core::onGroupMessage(Tox*, uint32_t groupId, uint32_t peerId, Tox_Message_Type type,
|
|
const uint8_t* cMessage, size_t length, void* vCore)
|
|
{
|
|
Core* core = static_cast<Core*>(vCore);
|
|
bool isAction = type == TOX_MESSAGE_TYPE_ACTION;
|
|
QString message = ToxString(cMessage, length).getQString();
|
|
emit core->groupMessageReceived(groupId, peerId, message, isAction);
|
|
}
|
|
|
|
void Core::onGroupPeerListChange(Tox*, uint32_t groupId, void* vCore)
|
|
{
|
|
const auto core = static_cast<Core*>(vCore);
|
|
qDebug() << QString("Group %1 peerlist changed").arg(groupId);
|
|
// no saveRequest, this callback is called on every connection to group peer, not just on brand new peers
|
|
emit core->groupPeerlistChanged(groupId);
|
|
}
|
|
|
|
void Core::onGroupPeerNameChange(Tox*, uint32_t groupId, uint32_t peerId, const uint8_t* name,
|
|
size_t length, void* vCore)
|
|
{
|
|
const auto newName = ToxString(name, length).getQString();
|
|
qDebug() << QString("Group %1, Peer %2, name changed to %3").arg(groupId).arg(peerId).arg(newName);
|
|
auto* core = static_cast<Core*>(vCore);
|
|
auto peerPk = core->getGroupPeerPk(groupId, peerId);
|
|
emit core->groupPeerNameChanged(groupId, peerPk, newName);
|
|
}
|
|
|
|
void Core::onGroupTitleChange(Tox*, uint32_t groupId, uint32_t peerId, const uint8_t* cTitle,
|
|
size_t length, void* vCore)
|
|
{
|
|
Core* core = static_cast<Core*>(vCore);
|
|
QString author = core->getGroupPeerName(groupId, peerId);
|
|
emit core->saveRequest();
|
|
emit core->groupTitleChanged(groupId, author, ToxString(cTitle, length).getQString());
|
|
}
|
|
|
|
void Core::onReadReceiptCallback(Tox*, uint32_t friendId, uint32_t receipt, void* core)
|
|
{
|
|
emit static_cast<Core*>(core)->receiptRecieved(friendId, ReceiptNum{receipt});
|
|
}
|
|
|
|
void Core::acceptFriendRequest(const ToxPk& friendPk)
|
|
{
|
|
QMutexLocker ml{&coreLoopLock};
|
|
// TODO: error handling
|
|
uint32_t friendId = tox_friend_add_norequest(tox.get(), friendPk.getData(), nullptr);
|
|
if (friendId == std::numeric_limits<uint32_t>::max()) {
|
|
emit failedToAddFriend(friendPk);
|
|
} else {
|
|
emit saveRequest();
|
|
emit friendAdded(friendId, friendPk);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @brief Checks that sending friendship request is correct and returns error message accordingly
|
|
* @param friendId Id of a friend which request is destined to
|
|
* @param message Friendship request message
|
|
* @return Returns empty string if sending request is correct, according error message otherwise
|
|
*/
|
|
QString Core::getFriendRequestErrorMessage(const ToxId& friendId, const QString& message) const
|
|
{
|
|
QMutexLocker ml{&coreLoopLock};
|
|
|
|
if (!friendId.isValid()) {
|
|
return tr("Invalid Tox ID", "Error while sending friendship request");
|
|
}
|
|
|
|
if (message.isEmpty()) {
|
|
return tr("You need to write a message with your request",
|
|
"Error while sending friendship request");
|
|
}
|
|
|
|
if (message.length() > static_cast<int>(tox_max_friend_request_length())) {
|
|
return tr("Your message is too long!", "Error while sending friendship request");
|
|
}
|
|
|
|
if (hasFriendWithPublicKey(friendId.getPublicKey())) {
|
|
return tr("Friend is already added", "Error while sending friendship request");
|
|
}
|
|
|
|
return QString{};
|
|
}
|
|
|
|
void Core::requestFriendship(const ToxId& friendId, const QString& message)
|
|
{
|
|
QMutexLocker ml{&coreLoopLock};
|
|
|
|
ToxPk friendPk = friendId.getPublicKey();
|
|
QString errorMessage = getFriendRequestErrorMessage(friendId, message);
|
|
if (!errorMessage.isNull()) {
|
|
emit failedToAddFriend(friendPk, errorMessage);
|
|
emit saveRequest();
|
|
return;
|
|
}
|
|
|
|
ToxString cMessage(message);
|
|
uint32_t friendNumber =
|
|
tox_friend_add(tox.get(), friendId.getBytes(), cMessage.data(), cMessage.size(), nullptr);
|
|
if (friendNumber == std::numeric_limits<uint32_t>::max()) {
|
|
qDebug() << "Failed to request friendship";
|
|
emit failedToAddFriend(friendPk);
|
|
} else {
|
|
qDebug() << "Requested friendship of " << friendNumber;
|
|
emit friendAdded(friendNumber, friendPk);
|
|
emit requestSent(friendPk, message);
|
|
}
|
|
|
|
emit saveRequest();
|
|
}
|
|
|
|
bool Core::sendMessageWithType(uint32_t friendId, const QString& message, Tox_Message_Type type,
|
|
ReceiptNum& receipt)
|
|
{
|
|
int size = message.toUtf8().size();
|
|
auto maxSize = tox_max_message_length();
|
|
if (size > maxSize) {
|
|
qCritical() << "Core::sendMessageWithType called with message of size:" << size
|
|
<< "when max is:" << maxSize << ". Ignoring.";
|
|
return false;
|
|
}
|
|
|
|
ToxString cMessage(message);
|
|
Tox_Err_Friend_Send_Message error;
|
|
receipt = ReceiptNum{tox_friend_send_message(tox.get(), friendId, type, cMessage.data(),
|
|
cMessage.size(), &error)};
|
|
if (parseFriendSendMessageError(error)) {
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool Core::sendMessage(uint32_t friendId, const QString& message, ReceiptNum& receipt)
|
|
{
|
|
QMutexLocker ml(&coreLoopLock);
|
|
return sendMessageWithType(friendId, message, TOX_MESSAGE_TYPE_NORMAL, receipt);
|
|
}
|
|
|
|
bool Core::sendAction(uint32_t friendId, const QString& action, ReceiptNum& receipt)
|
|
{
|
|
QMutexLocker ml(&coreLoopLock);
|
|
return sendMessageWithType(friendId, action, TOX_MESSAGE_TYPE_ACTION, receipt);
|
|
}
|
|
|
|
void Core::sendTyping(uint32_t friendId, bool typing)
|
|
{
|
|
QMutexLocker ml{&coreLoopLock};
|
|
|
|
if (!tox_self_set_typing(tox.get(), friendId, typing, nullptr)) {
|
|
emit failedToSetTyping(typing);
|
|
}
|
|
}
|
|
|
|
void Core::sendGroupMessageWithType(int groupId, const QString& message, Tox_Message_Type type)
|
|
{
|
|
QMutexLocker ml{&coreLoopLock};
|
|
|
|
int size = message.toUtf8().size();
|
|
auto maxSize = tox_max_message_length();
|
|
if (size > maxSize) {
|
|
qCritical() << "Core::sendMessageWithType called with message of size:" << size
|
|
<< "when max is:" << maxSize << ". Ignoring.";
|
|
return;
|
|
}
|
|
|
|
ToxString cMsg(message);
|
|
Tox_Err_Conference_Send_Message error;
|
|
tox_conference_send_message(tox.get(), groupId, type, cMsg.data(), cMsg.size(), &error);
|
|
if (!parseConferenceSendMessageError(error)) {
|
|
emit groupSentFailed(groupId);
|
|
return;
|
|
}
|
|
}
|
|
|
|
void Core::sendGroupMessage(int groupId, const QString& message)
|
|
{
|
|
QMutexLocker ml{&coreLoopLock};
|
|
|
|
sendGroupMessageWithType(groupId, message, TOX_MESSAGE_TYPE_NORMAL);
|
|
}
|
|
|
|
void Core::sendGroupAction(int groupId, const QString& message)
|
|
{
|
|
QMutexLocker ml{&coreLoopLock};
|
|
|
|
sendGroupMessageWithType(groupId, message, TOX_MESSAGE_TYPE_ACTION);
|
|
}
|
|
|
|
void Core::changeGroupTitle(int groupId, const QString& title)
|
|
{
|
|
QMutexLocker ml{&coreLoopLock};
|
|
|
|
ToxString cTitle(title);
|
|
Tox_Err_Conference_Title error;
|
|
bool success = tox_conference_set_title(tox.get(), groupId, cTitle.data(), cTitle.size(), &error);
|
|
if (success && error == TOX_ERR_CONFERENCE_TITLE_OK) {
|
|
emit saveRequest();
|
|
emit groupTitleChanged(groupId, getUsername(), title);
|
|
return;
|
|
}
|
|
|
|
qCritical() << "Fail of tox_conference_set_title";
|
|
switch (error) {
|
|
case TOX_ERR_CONFERENCE_TITLE_CONFERENCE_NOT_FOUND:
|
|
qCritical() << "Conference not found";
|
|
break;
|
|
|
|
case TOX_ERR_CONFERENCE_TITLE_FAIL_SEND:
|
|
qCritical() << "Conference title failed to send";
|
|
break;
|
|
|
|
case TOX_ERR_CONFERENCE_TITLE_INVALID_LENGTH:
|
|
qCritical() << "Invalid length";
|
|
break;
|
|
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
void Core::removeFriend(uint32_t friendId)
|
|
{
|
|
QMutexLocker ml{&coreLoopLock};
|
|
|
|
if (!tox_friend_delete(tox.get(), friendId, nullptr)) {
|
|
emit failedToRemoveFriend(friendId);
|
|
return;
|
|
}
|
|
|
|
emit saveRequest();
|
|
emit friendRemoved(friendId);
|
|
}
|
|
|
|
void Core::removeGroup(int groupId)
|
|
{
|
|
QMutexLocker ml{&coreLoopLock};
|
|
|
|
Tox_Err_Conference_Delete error;
|
|
bool success = tox_conference_delete(tox.get(), groupId, &error);
|
|
if (success && error == TOX_ERR_CONFERENCE_DELETE_OK) {
|
|
emit saveRequest();
|
|
av->leaveGroupCall(groupId);
|
|
return;
|
|
}
|
|
|
|
qCritical() << "Fail of tox_conference_delete";
|
|
switch (error) {
|
|
case TOX_ERR_CONFERENCE_DELETE_CONFERENCE_NOT_FOUND:
|
|
qCritical() << "Conference not found";
|
|
break;
|
|
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @brief Returns our username, or an empty string on failure
|
|
*/
|
|
QString Core::getUsername() const
|
|
{
|
|
QMutexLocker ml{&coreLoopLock};
|
|
|
|
QString sname;
|
|
if (!tox) {
|
|
return sname;
|
|
}
|
|
|
|
int size = tox_self_get_name_size(tox.get());
|
|
uint8_t* name = new uint8_t[size];
|
|
tox_self_get_name(tox.get(), name);
|
|
sname = ToxString(name, size).getQString();
|
|
delete[] name;
|
|
return sname;
|
|
}
|
|
|
|
void Core::setUsername(const QString& username)
|
|
{
|
|
QMutexLocker ml{&coreLoopLock};
|
|
|
|
if (username == getUsername()) {
|
|
return;
|
|
}
|
|
|
|
ToxString cUsername(username);
|
|
if (!tox_self_set_name(tox.get(), cUsername.data(), cUsername.size(), nullptr)) {
|
|
emit failedToSetUsername(username);
|
|
return;
|
|
}
|
|
|
|
emit usernameSet(username);
|
|
emit saveRequest();
|
|
}
|
|
|
|
/**
|
|
* @brief Returns our Tox ID
|
|
*/
|
|
ToxId Core::getSelfId() const
|
|
{
|
|
QMutexLocker ml{&coreLoopLock};
|
|
|
|
uint8_t friendId[TOX_ADDRESS_SIZE] = {0x00};
|
|
tox_self_get_address(tox.get(), friendId);
|
|
return ToxId(friendId, TOX_ADDRESS_SIZE);
|
|
}
|
|
|
|
/**
|
|
* @brief Gets self public key
|
|
* @return Self PK
|
|
*/
|
|
ToxPk Core::getSelfPublicKey() const
|
|
{
|
|
QMutexLocker ml{&coreLoopLock};
|
|
|
|
uint8_t friendId[TOX_ADDRESS_SIZE] = {0x00};
|
|
tox_self_get_address(tox.get(), friendId);
|
|
return ToxPk(friendId);
|
|
}
|
|
|
|
/**
|
|
* @brief Returns our public and private keys
|
|
*/
|
|
QPair<QByteArray, QByteArray> Core::getKeypair() const
|
|
{
|
|
QMutexLocker ml{&coreLoopLock};
|
|
|
|
QPair<QByteArray, QByteArray> keypair;
|
|
assert(tox != nullptr);
|
|
|
|
QByteArray pk(TOX_PUBLIC_KEY_SIZE, 0x00);
|
|
QByteArray sk(TOX_SECRET_KEY_SIZE, 0x00);
|
|
tox_self_get_public_key(tox.get(), reinterpret_cast<uint8_t*>(pk.data()));
|
|
tox_self_get_secret_key(tox.get(), reinterpret_cast<uint8_t*>(sk.data()));
|
|
keypair.first = pk;
|
|
keypair.second = sk;
|
|
return keypair;
|
|
}
|
|
|
|
/**
|
|
* @brief Returns our status message, or an empty string on failure
|
|
*/
|
|
QString Core::getStatusMessage() const
|
|
{
|
|
QMutexLocker ml{&coreLoopLock};
|
|
|
|
assert(tox != nullptr);
|
|
|
|
size_t size = tox_self_get_status_message_size(tox.get());
|
|
if (size == 0) {
|
|
return {};
|
|
}
|
|
uint8_t* name = new uint8_t[size];
|
|
tox_self_get_status_message(tox.get(), name);
|
|
QString sname = ToxString(name, size).getQString();
|
|
delete[] name;
|
|
return sname;
|
|
}
|
|
|
|
/**
|
|
* @brief Returns our user status
|
|
*/
|
|
Status::Status Core::getStatus() const
|
|
{
|
|
QMutexLocker ml{&coreLoopLock};
|
|
|
|
return static_cast<Status::Status>(tox_self_get_status(tox.get()));
|
|
}
|
|
|
|
void Core::setStatusMessage(const QString& message)
|
|
{
|
|
QMutexLocker ml{&coreLoopLock};
|
|
|
|
if (message == getStatusMessage()) {
|
|
return;
|
|
}
|
|
|
|
ToxString cMessage(message);
|
|
if (!tox_self_set_status_message(tox.get(), cMessage.data(), cMessage.size(), nullptr)) {
|
|
emit failedToSetStatusMessage(message);
|
|
return;
|
|
}
|
|
|
|
emit saveRequest();
|
|
emit statusMessageSet(message);
|
|
}
|
|
|
|
void Core::setStatus(Status::Status status)
|
|
{
|
|
QMutexLocker ml{&coreLoopLock};
|
|
|
|
Tox_User_Status userstatus;
|
|
switch (status) {
|
|
case Status::Status::Online:
|
|
userstatus = TOX_USER_STATUS_NONE;
|
|
break;
|
|
|
|
case Status::Status::Away:
|
|
userstatus = TOX_USER_STATUS_AWAY;
|
|
break;
|
|
|
|
case Status::Status::Busy:
|
|
userstatus = TOX_USER_STATUS_BUSY;
|
|
break;
|
|
|
|
default:
|
|
return;
|
|
break;
|
|
}
|
|
|
|
tox_self_set_status(tox.get(), userstatus);
|
|
emit saveRequest();
|
|
emit statusSet(status);
|
|
}
|
|
|
|
/**
|
|
* @brief Returns the unencrypted tox save data
|
|
*/
|
|
QByteArray Core::getToxSaveData()
|
|
{
|
|
QMutexLocker ml{&coreLoopLock};
|
|
|
|
uint32_t fileSize = tox_get_savedata_size(tox.get());
|
|
QByteArray data;
|
|
data.resize(fileSize);
|
|
tox_get_savedata(tox.get(), (uint8_t*)data.data());
|
|
return data;
|
|
}
|
|
|
|
// Declared to avoid code duplication
|
|
#define GET_FRIEND_PROPERTY(property, function, checkSize) \
|
|
const size_t property##Size = function##_size(tox.get(), ids[i], nullptr); \
|
|
if ((!checkSize || property##Size) && property##Size != SIZE_MAX) { \
|
|
uint8_t* prop = new uint8_t[property##Size]; \
|
|
if (function(tox.get(), ids[i], prop, nullptr)) { \
|
|
QString propStr = ToxString(prop, property##Size).getQString(); \
|
|
emit friend##property##Changed(ids[i], propStr); \
|
|
} \
|
|
\
|
|
delete[] prop; \
|
|
}
|
|
|
|
void Core::loadFriends()
|
|
{
|
|
QMutexLocker ml{&coreLoopLock};
|
|
|
|
const size_t friendCount = tox_self_get_friend_list_size(tox.get());
|
|
if (friendCount == 0) {
|
|
return;
|
|
}
|
|
|
|
uint32_t* ids = new uint32_t[friendCount];
|
|
tox_self_get_friend_list(tox.get(), ids);
|
|
uint8_t friendPk[TOX_PUBLIC_KEY_SIZE] = {0x00};
|
|
for (size_t i = 0; i < friendCount; ++i) {
|
|
if (!tox_friend_get_public_key(tox.get(), ids[i], friendPk, nullptr)) {
|
|
continue;
|
|
}
|
|
|
|
emit friendAdded(ids[i], ToxPk(friendPk));
|
|
GET_FRIEND_PROPERTY(Username, tox_friend_get_name, true);
|
|
GET_FRIEND_PROPERTY(StatusMessage, tox_friend_get_status_message, false);
|
|
checkLastOnline(ids[i]);
|
|
}
|
|
delete[] ids;
|
|
}
|
|
|
|
void Core::loadGroups()
|
|
{
|
|
QMutexLocker ml{&coreLoopLock};
|
|
|
|
const size_t groupCount = tox_conference_get_chatlist_size(tox.get());
|
|
if (groupCount == 0) {
|
|
return;
|
|
}
|
|
|
|
auto groupNumbers = new uint32_t[groupCount];
|
|
tox_conference_get_chatlist(tox.get(), groupNumbers);
|
|
|
|
for (size_t i = 0; i < groupCount; ++i) {
|
|
TOX_ERR_CONFERENCE_TITLE error;
|
|
QString name;
|
|
const auto groupNumber = groupNumbers[i];
|
|
size_t titleSize = tox_conference_get_title_size(tox.get(), groupNumber, &error);
|
|
const GroupId persistentId = getGroupPersistentId(groupNumber);
|
|
const QString defaultName = tr("Groupchat %1").arg(persistentId.toString().left(8));
|
|
if (LogConferenceTitleError(error)) {
|
|
name = defaultName;
|
|
} else {
|
|
QByteArray nameByteArray = QByteArray(static_cast<int>(titleSize), Qt::Uninitialized);
|
|
tox_conference_get_title(tox.get(), groupNumber,
|
|
reinterpret_cast<uint8_t*>(nameByteArray.data()), &error);
|
|
if (LogConferenceTitleError(error)) {
|
|
name = defaultName;
|
|
} else {
|
|
name = ToxString(nameByteArray).getQString();
|
|
}
|
|
}
|
|
if (getGroupAvEnabled(groupNumber)) {
|
|
if (toxav_groupchat_enable_av(tox.get(), groupNumber, CoreAV::groupCallCallback, this)) {
|
|
qCritical() << "Failed to enable audio on loaded group" << groupNumber;
|
|
}
|
|
}
|
|
emit emptyGroupCreated(groupNumber, persistentId, name);
|
|
}
|
|
|
|
delete[] groupNumbers;
|
|
}
|
|
|
|
void Core::checkLastOnline(uint32_t friendId)
|
|
{
|
|
QMutexLocker ml{&coreLoopLock};
|
|
|
|
const uint64_t lastOnline = tox_friend_get_last_online(tox.get(), friendId, nullptr);
|
|
if (lastOnline != std::numeric_limits<uint64_t>::max()) {
|
|
emit friendLastSeenChanged(friendId, QDateTime::fromTime_t(lastOnline));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @brief Returns the list of friendIds in our friendlist, an empty list on error
|
|
*/
|
|
QVector<uint32_t> Core::getFriendList() const
|
|
{
|
|
QMutexLocker ml{&coreLoopLock};
|
|
|
|
QVector<uint32_t> friends;
|
|
friends.resize(tox_self_get_friend_list_size(tox.get()));
|
|
tox_self_get_friend_list(tox.get(), friends.data());
|
|
return friends;
|
|
}
|
|
|
|
/**
|
|
* @brief Print in console text of error.
|
|
* @param error Error to handle.
|
|
* @return True if no error, false otherwise.
|
|
*/
|
|
bool Core::parsePeerQueryError(Tox_Err_Conference_Peer_Query error) const
|
|
{
|
|
switch (error) {
|
|
case TOX_ERR_CONFERENCE_PEER_QUERY_OK:
|
|
return true;
|
|
|
|
case TOX_ERR_CONFERENCE_PEER_QUERY_CONFERENCE_NOT_FOUND:
|
|
qCritical() << "Conference not found";
|
|
return false;
|
|
|
|
case TOX_ERR_CONFERENCE_PEER_QUERY_NO_CONNECTION:
|
|
qCritical() << "No connection";
|
|
return false;
|
|
|
|
case TOX_ERR_CONFERENCE_PEER_QUERY_PEER_NOT_FOUND:
|
|
qCritical() << "Peer not found";
|
|
return false;
|
|
|
|
default:
|
|
qCritical() << "Unknow error code:" << error;
|
|
return false;
|
|
}
|
|
}
|
|
|
|
GroupId Core::getGroupPersistentId(uint32_t groupNumber) const
|
|
{
|
|
QMutexLocker ml{&coreLoopLock};
|
|
|
|
size_t conferenceIdSize = TOX_CONFERENCE_UID_SIZE;
|
|
QByteArray groupPersistentId(conferenceIdSize, Qt::Uninitialized);
|
|
if (tox_conference_get_id(tox.get(), groupNumber,
|
|
reinterpret_cast<uint8_t*>(groupPersistentId.data()))) {
|
|
return GroupId{groupPersistentId};
|
|
} else {
|
|
qCritical() << "Failed to get conference ID of group" << groupNumber;
|
|
return {};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @brief Get number of peers in the conference.
|
|
* @return The number of peers in the conference. UINT32_MAX on failure.
|
|
*/
|
|
uint32_t Core::getGroupNumberPeers(int groupId) const
|
|
{
|
|
QMutexLocker ml{&coreLoopLock};
|
|
|
|
Tox_Err_Conference_Peer_Query error;
|
|
uint32_t count = tox_conference_peer_count(tox.get(), groupId, &error);
|
|
if (!parsePeerQueryError(error)) {
|
|
return std::numeric_limits<uint32_t>::max();
|
|
}
|
|
|
|
return count;
|
|
}
|
|
|
|
/**
|
|
* @brief Get the name of a peer of a group
|
|
*/
|
|
QString Core::getGroupPeerName(int groupId, int peerId) const
|
|
{
|
|
QMutexLocker ml{&coreLoopLock};
|
|
|
|
// from tox.h: "If peer_number == UINT32_MAX, then author is unknown (e.g. initial joining the conference)."
|
|
if (peerId == std::numeric_limits<uint32_t>::max()) {
|
|
return {};
|
|
}
|
|
|
|
Tox_Err_Conference_Peer_Query error;
|
|
size_t length = tox_conference_peer_get_name_size(tox.get(), groupId, peerId, &error);
|
|
if (!parsePeerQueryError(error)) {
|
|
return QString{};
|
|
}
|
|
|
|
QByteArray name(length, Qt::Uninitialized);
|
|
uint8_t* namePtr = reinterpret_cast<uint8_t*>(name.data());
|
|
bool success = tox_conference_peer_get_name(tox.get(), groupId, peerId, namePtr, &error);
|
|
if (!parsePeerQueryError(error)) {
|
|
return QString{};
|
|
}
|
|
assert(success);
|
|
|
|
return ToxString(name).getQString();
|
|
}
|
|
|
|
/**
|
|
* @brief Get the public key of a peer of a group
|
|
*/
|
|
ToxPk Core::getGroupPeerPk(int groupId, int peerId) const
|
|
{
|
|
QMutexLocker ml{&coreLoopLock};
|
|
|
|
uint8_t friendPk[TOX_PUBLIC_KEY_SIZE] = {0x00};
|
|
Tox_Err_Conference_Peer_Query error;
|
|
bool success = tox_conference_peer_get_public_key(tox.get(), groupId, peerId, friendPk, &error);
|
|
if (!parsePeerQueryError(error)) {
|
|
return ToxPk{};
|
|
}
|
|
assert(success);
|
|
|
|
return ToxPk(friendPk);
|
|
}
|
|
|
|
/**
|
|
* @brief Get the names of the peers of a group
|
|
*/
|
|
QStringList Core::getGroupPeerNames(int groupId) const
|
|
{
|
|
QMutexLocker ml{&coreLoopLock};
|
|
|
|
assert(tox != nullptr);
|
|
|
|
uint32_t nPeers = getGroupNumberPeers(groupId);
|
|
if (nPeers == std::numeric_limits<uint32_t>::max()) {
|
|
qWarning() << "getGroupPeerNames: Unable to get number of peers";
|
|
return {};
|
|
}
|
|
|
|
QStringList names;
|
|
for (uint32_t i = 0; i < nPeers; ++i) {
|
|
TOX_ERR_CONFERENCE_PEER_QUERY error;
|
|
size_t length = tox_conference_peer_get_name_size(tox.get(), groupId, i, &error);
|
|
if (!parsePeerQueryError(error)) {
|
|
names.append(QString());
|
|
continue;
|
|
}
|
|
|
|
QByteArray name(length, Qt::Uninitialized);
|
|
uint8_t* namePtr = reinterpret_cast<uint8_t*>(name.data());
|
|
bool ok = tox_conference_peer_get_name(tox.get(), groupId, i, namePtr, &error);
|
|
if (ok && parsePeerQueryError(error)) {
|
|
names.append(ToxString(name).getQString());
|
|
} else {
|
|
names.append(QString());
|
|
}
|
|
}
|
|
|
|
assert(names.size() == nPeers);
|
|
|
|
return names;
|
|
}
|
|
|
|
/**
|
|
* @brief Check, that group has audio or video stream
|
|
* @param groupId Id of group to check
|
|
* @return True for AV groups, false for text-only groups
|
|
*/
|
|
bool Core::getGroupAvEnabled(int groupId) const
|
|
{
|
|
QMutexLocker ml{&coreLoopLock};
|
|
TOX_ERR_CONFERENCE_GET_TYPE error;
|
|
TOX_CONFERENCE_TYPE type = tox_conference_get_type(tox.get(), groupId, &error);
|
|
switch (error) {
|
|
case TOX_ERR_CONFERENCE_GET_TYPE_OK:
|
|
break;
|
|
case TOX_ERR_CONFERENCE_GET_TYPE_CONFERENCE_NOT_FOUND:
|
|
qWarning() << "Conference not found";
|
|
break;
|
|
default:
|
|
qWarning() << "Unknown error code:" << QString::number(error);
|
|
break;
|
|
}
|
|
|
|
return type == TOX_CONFERENCE_TYPE_AV;
|
|
}
|
|
|
|
/**
|
|
* @brief Print in console text of error.
|
|
* @param error Error to handle.
|
|
* @return True if no error, false otherwise.
|
|
*/
|
|
bool Core::parseConferenceJoinError(Tox_Err_Conference_Join error) const
|
|
{
|
|
switch (error) {
|
|
case TOX_ERR_CONFERENCE_JOIN_OK:
|
|
return true;
|
|
|
|
case TOX_ERR_CONFERENCE_JOIN_DUPLICATE:
|
|
qCritical() << "Conference duplicate";
|
|
return false;
|
|
|
|
case TOX_ERR_CONFERENCE_JOIN_FAIL_SEND:
|
|
qCritical() << "Conference join failed to send";
|
|
return false;
|
|
|
|
case TOX_ERR_CONFERENCE_JOIN_FRIEND_NOT_FOUND:
|
|
qCritical() << "Friend not found";
|
|
return false;
|
|
|
|
case TOX_ERR_CONFERENCE_JOIN_INIT_FAIL:
|
|
qCritical() << "Init fail";
|
|
return false;
|
|
|
|
case TOX_ERR_CONFERENCE_JOIN_INVALID_LENGTH:
|
|
qCritical() << "Invalid length";
|
|
return false;
|
|
|
|
case TOX_ERR_CONFERENCE_JOIN_WRONG_TYPE:
|
|
qCritical() << "Wrong conference type";
|
|
return false;
|
|
|
|
default:
|
|
qCritical() << "Unknow error code:" << error;
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @brief Accept a groupchat invite.
|
|
* @param inviteInfo Object which contains info about group invitation
|
|
*
|
|
* @return Conference number on success, UINT32_MAX on failure.
|
|
*/
|
|
uint32_t Core::joinGroupchat(const GroupInvite& inviteInfo)
|
|
{
|
|
QMutexLocker ml{&coreLoopLock};
|
|
|
|
const uint32_t friendId = inviteInfo.getFriendId();
|
|
const uint8_t confType = inviteInfo.getType();
|
|
const QByteArray invite = inviteInfo.getInvite();
|
|
const uint8_t* const cookie = reinterpret_cast<const uint8_t*>(invite.data());
|
|
const size_t cookieLength = invite.length();
|
|
uint32_t groupNum{std::numeric_limits<uint32_t>::max()};
|
|
switch (confType) {
|
|
case TOX_CONFERENCE_TYPE_TEXT: {
|
|
qDebug() << QString("Trying to join text groupchat invite sent by friend %1").arg(friendId);
|
|
Tox_Err_Conference_Join error;
|
|
groupNum = tox_conference_join(tox.get(), friendId, cookie, cookieLength, &error);
|
|
if (!parseConferenceJoinError(error)) {
|
|
groupNum = std::numeric_limits<uint32_t>::max();
|
|
}
|
|
break;
|
|
}
|
|
case TOX_CONFERENCE_TYPE_AV: {
|
|
qDebug() << QString("Trying to join AV groupchat invite sent by friend %1").arg(friendId);
|
|
groupNum = toxav_join_av_groupchat(tox.get(), friendId, cookie, cookieLength,
|
|
CoreAV::groupCallCallback, const_cast<Core*>(this));
|
|
break;
|
|
}
|
|
default:
|
|
qWarning() << "joinGroupchat: Unknown groupchat type " << confType;
|
|
}
|
|
if (groupNum != std::numeric_limits<uint32_t>::max()) {
|
|
emit saveRequest();
|
|
emit groupJoined(groupNum, getGroupPersistentId(groupNum));
|
|
}
|
|
return groupNum;
|
|
}
|
|
|
|
void Core::groupInviteFriend(uint32_t friendId, int groupId)
|
|
{
|
|
QMutexLocker ml{&coreLoopLock};
|
|
|
|
Tox_Err_Conference_Invite error;
|
|
tox_conference_invite(tox.get(), friendId, groupId, &error);
|
|
|
|
switch (error) {
|
|
case TOX_ERR_CONFERENCE_INVITE_OK:
|
|
break;
|
|
|
|
case TOX_ERR_CONFERENCE_INVITE_CONFERENCE_NOT_FOUND:
|
|
qCritical() << "Conference not found";
|
|
break;
|
|
|
|
case TOX_ERR_CONFERENCE_INVITE_FAIL_SEND:
|
|
qCritical() << "Conference invite failed to send";
|
|
break;
|
|
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
int Core::createGroup(uint8_t type)
|
|
{
|
|
QMutexLocker ml{&coreLoopLock};
|
|
|
|
if (type == TOX_CONFERENCE_TYPE_TEXT) {
|
|
Tox_Err_Conference_New error;
|
|
uint32_t groupId = tox_conference_new(tox.get(), &error);
|
|
|
|
switch (error) {
|
|
case TOX_ERR_CONFERENCE_NEW_OK:
|
|
emit saveRequest();
|
|
emit emptyGroupCreated(groupId, getGroupPersistentId(groupId));
|
|
return groupId;
|
|
|
|
case TOX_ERR_CONFERENCE_NEW_INIT:
|
|
qCritical() << "The conference instance failed to initialize";
|
|
return std::numeric_limits<uint32_t>::max();
|
|
|
|
default:
|
|
return std::numeric_limits<uint32_t>::max();
|
|
}
|
|
} else if (type == TOX_CONFERENCE_TYPE_AV) {
|
|
uint32_t groupId = toxav_add_av_groupchat(tox.get(), CoreAV::groupCallCallback, this);
|
|
emit saveRequest();
|
|
emit emptyGroupCreated(groupId, getGroupPersistentId(groupId));
|
|
return groupId;
|
|
} else {
|
|
qWarning() << "createGroup: Unknown type " << type;
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @brief Checks if a friend is online. Unknown friends are considered offline.
|
|
*/
|
|
bool Core::isFriendOnline(uint32_t friendId) const
|
|
{
|
|
QMutexLocker ml{&coreLoopLock};
|
|
|
|
Tox_Connection connection = tox_friend_get_connection_status(tox.get(), friendId, nullptr);
|
|
return connection != TOX_CONNECTION_NONE;
|
|
}
|
|
|
|
/**
|
|
* @brief Checks if we have a friend by public key
|
|
*/
|
|
bool Core::hasFriendWithPublicKey(const ToxPk& publicKey) const
|
|
{
|
|
QMutexLocker ml{&coreLoopLock};
|
|
|
|
if (publicKey.isEmpty()) {
|
|
return false;
|
|
}
|
|
|
|
// TODO: error handling
|
|
uint32_t friendId = tox_friend_by_public_key(tox.get(), publicKey.getData(), nullptr);
|
|
return friendId != std::numeric_limits<uint32_t>::max();
|
|
}
|
|
|
|
/**
|
|
* @brief Get the public key part of the ToxID only
|
|
*/
|
|
ToxPk Core::getFriendPublicKey(uint32_t friendNumber) const
|
|
{
|
|
QMutexLocker ml{&coreLoopLock};
|
|
|
|
uint8_t rawid[TOX_PUBLIC_KEY_SIZE];
|
|
if (!tox_friend_get_public_key(tox.get(), friendNumber, rawid, nullptr)) {
|
|
qWarning() << "getFriendPublicKey: Getting public key failed";
|
|
return ToxPk();
|
|
}
|
|
|
|
return ToxPk(rawid);
|
|
}
|
|
|
|
/**
|
|
* @brief Get the username of a friend
|
|
*/
|
|
QString Core::getFriendUsername(uint32_t friendnumber) const
|
|
{
|
|
QMutexLocker ml{&coreLoopLock};
|
|
|
|
size_t namesize = tox_friend_get_name_size(tox.get(), friendnumber, nullptr);
|
|
if (namesize == SIZE_MAX) {
|
|
qWarning() << "getFriendUsername: Failed to get name size for friend " << friendnumber;
|
|
return QString();
|
|
}
|
|
|
|
uint8_t* name = new uint8_t[namesize];
|
|
tox_friend_get_name(tox.get(), friendnumber, name, nullptr);
|
|
ToxString sname(name, namesize);
|
|
delete[] name;
|
|
return sname.getQString();
|
|
}
|
|
|
|
QStringList Core::splitMessage(const QString& message)
|
|
{
|
|
QStringList splittedMsgs;
|
|
QByteArray ba_message{message.toUtf8()};
|
|
|
|
/*
|
|
* TODO: Remove this hack; the reported max message length we receive from c-toxcore
|
|
* as of 08-02-2019 is inaccurate, causing us to generate too large messages when splitting
|
|
* them up.
|
|
*
|
|
* The inconsistency lies in c-toxcore group.c:2480 using MAX_GROUP_MESSAGE_DATA_LEN to verify
|
|
* message size is within limit, but tox_max_message_length giving a different size limit to us.
|
|
*
|
|
* (uint32_t tox_max_message_length(void); declared in tox.h, unable to see explicit definition)
|
|
*/
|
|
const auto maxLen = tox_max_message_length() - 50;
|
|
|
|
while (ba_message.size() > maxLen) {
|
|
int splitPos = ba_message.lastIndexOf('\n', maxLen - 1);
|
|
|
|
if (splitPos <= 0) {
|
|
splitPos = ba_message.lastIndexOf(' ', maxLen - 1);
|
|
}
|
|
|
|
if (splitPos <= 0) {
|
|
constexpr uint8_t firstOfMultiByteMask = 0xC0;
|
|
constexpr uint8_t multiByteMask = 0x80;
|
|
splitPos = maxLen;
|
|
// don't split a utf8 character
|
|
if ((ba_message[splitPos] & multiByteMask) == multiByteMask) {
|
|
while ((ba_message[splitPos] & firstOfMultiByteMask) != firstOfMultiByteMask) {
|
|
--splitPos;
|
|
}
|
|
}
|
|
--splitPos;
|
|
}
|
|
splittedMsgs.append(QString{ba_message.left(splitPos + 1)});
|
|
ba_message = ba_message.mid(splitPos + 1);
|
|
}
|
|
|
|
splittedMsgs.append(QString{ba_message});
|
|
return splittedMsgs;
|
|
}
|
|
|
|
QString Core::getPeerName(const ToxPk& id) const
|
|
{
|
|
QMutexLocker ml{&coreLoopLock};
|
|
|
|
QString name;
|
|
uint32_t friendId = tox_friend_by_public_key(tox.get(), id.getData(), nullptr);
|
|
if (friendId == std::numeric_limits<uint32_t>::max()) {
|
|
qWarning() << "getPeerName: No such peer";
|
|
return name;
|
|
}
|
|
|
|
const size_t nameSize = tox_friend_get_name_size(tox.get(), friendId, nullptr);
|
|
if (nameSize == SIZE_MAX) {
|
|
return name;
|
|
}
|
|
|
|
uint8_t* cname = new uint8_t[nameSize < tox_max_name_length() ? tox_max_name_length() : nameSize];
|
|
if (!tox_friend_get_name(tox.get(), friendId, cname, nullptr)) {
|
|
qWarning() << "getPeerName: Can't get name of friend " + QString().setNum(friendId);
|
|
delete[] cname;
|
|
return name;
|
|
}
|
|
|
|
name = ToxString(cname, nameSize).getQString();
|
|
delete[] cname;
|
|
return name;
|
|
}
|
|
|
|
/**
|
|
* @brief Sets the NoSpam value to prevent friend request spam
|
|
* @param nospam an arbitrary which becomes part of the Tox ID
|
|
*/
|
|
void Core::setNospam(uint32_t nospam)
|
|
{
|
|
QMutexLocker ml{&coreLoopLock};
|
|
|
|
tox_self_set_nospam(tox.get(), nospam);
|
|
emit idSet(getSelfId());
|
|
}
|