mirror of
https://github.com/qTox/qTox.git
synced 2024-03-22 14:00:36 +08:00
Make Nexus own and start the LoginScreen
And start implementing some of the required methods to make Core, LoginScreen and Nexus use Profile
This commit is contained in:
parent
7d6167d90c
commit
032c561e62
|
@ -24,6 +24,7 @@
|
|||
#include "src/audio.h"
|
||||
#include "src/profilelocker.h"
|
||||
#include "src/avatarbroadcaster.h"
|
||||
#include "src/profile.h"
|
||||
#include "corefile.h"
|
||||
|
||||
#include <tox/tox.h>
|
||||
|
@ -52,11 +53,9 @@ QThread* Core::coreThread{nullptr};
|
|||
|
||||
#define MAX_GROUP_MESSAGE_LEN 1024
|
||||
|
||||
Core::Core(QThread *CoreThread, QString loadPath) :
|
||||
tox(nullptr), toxav(nullptr), loadPath(loadPath), ready{false}
|
||||
Core::Core(QThread *CoreThread, Profile& profile) :
|
||||
tox(nullptr), toxav(nullptr), profile(profile), ready{false}
|
||||
{
|
||||
qDebug() << "loading Tox from" << loadPath;
|
||||
|
||||
coreThread = CoreThread;
|
||||
|
||||
Audio::getInstance();
|
||||
|
@ -239,7 +238,7 @@ void Core::start()
|
|||
{
|
||||
qDebug() << "Starting up";
|
||||
|
||||
QByteArray savedata = loadToxSave(loadPath);
|
||||
QByteArray savedata = profile.loadToxSave();
|
||||
|
||||
make_tox(savedata);
|
||||
|
||||
|
@ -888,7 +887,7 @@ QString Core::sanitize(QString name)
|
|||
QByteArray Core::loadToxSave(QString path)
|
||||
{
|
||||
QByteArray data;
|
||||
loadPath = ""; // if not empty upon return, then user forgot a password and is switching
|
||||
//loadPath = ""; // if not empty upon return, then user forgot a password and is switching
|
||||
|
||||
// If we can't get a lock, then another instance is already using that profile
|
||||
while (!ProfileLocker::lock(QFileInfo(path).baseName()))
|
||||
|
@ -1019,10 +1018,10 @@ void Core::switchConfiguration(const QString& _profile)
|
|||
emit selfAvatarChanged(QPixmap(":/img/contact_dark.svg"));
|
||||
emit blockingClearContacts(); // we need this to block, but signals are required for thread safety
|
||||
|
||||
if (profile.isEmpty())
|
||||
loadPath = "";
|
||||
else
|
||||
loadPath = QDir(Settings::getSettingsDirPath()).filePath(profile + TOX_EXT);
|
||||
//if (profile.isEmpty())
|
||||
//loadPath = "";
|
||||
//else
|
||||
// loadPath = QDir(Settings::getSettingsDirPath()).filePath(profile + TOX_EXT);
|
||||
|
||||
Settings::getInstance().switchProfile(profile);
|
||||
HistoryKeeper::resetInstance();
|
||||
|
|
|
@ -29,6 +29,7 @@
|
|||
#include "coredefines.h"
|
||||
#include "toxid.h"
|
||||
|
||||
class Profile;
|
||||
template <typename T> class QList;
|
||||
class QTimer;
|
||||
class QString;
|
||||
|
@ -45,7 +46,7 @@ class Core : public QObject
|
|||
public:
|
||||
enum PasswordType {ptMain = 0, ptHistory, ptCounter};
|
||||
|
||||
explicit Core(QThread* coreThread, QString initialLoadPath);
|
||||
explicit Core(QThread* coreThread, Profile& profile);
|
||||
static Core* getInstance(); ///< Returns the global widget's Core instance
|
||||
~Core();
|
||||
|
||||
|
@ -292,7 +293,7 @@ private:
|
|||
Tox* tox;
|
||||
ToxAv* toxav;
|
||||
QTimer *toxTimer, *fileTimer; //, *saveTimer;
|
||||
QString loadPath; // meaningless after start() is called
|
||||
Profile& profile;
|
||||
int dhtServerId;
|
||||
static ToxCall calls[TOXAV_MAX_CALLS];
|
||||
#ifdef QTOX_FILTER_AUDIO
|
||||
|
|
|
@ -110,7 +110,7 @@ QByteArray Core::encryptData(const QByteArray& data, PasswordType passtype)
|
|||
if (!tox_pass_key_encrypt(reinterpret_cast<const uint8_t*>(data.data()), data.size(),
|
||||
pwsaltedkeys[passtype], encrypted, nullptr))
|
||||
{
|
||||
qWarning() << "encryptData: encryption failed";
|
||||
qWarning() << "Encryption failed";
|
||||
return QByteArray();
|
||||
}
|
||||
return QByteArray(reinterpret_cast<char*>(encrypted), data.size() + TOX_PASS_ENCRYPTION_EXTRA_LENGTH);
|
||||
|
@ -126,7 +126,7 @@ QByteArray Core::decryptData(const QByteArray& data, PasswordType passtype)
|
|||
if (!tox_pass_key_decrypt(reinterpret_cast<const uint8_t*>(data.data()), data.size(),
|
||||
pwsaltedkeys[passtype], decrypted, nullptr))
|
||||
{
|
||||
qWarning() << "decryptData: decryption failed";
|
||||
qWarning() << "Decryption failed";
|
||||
return QByteArray();
|
||||
}
|
||||
return QByteArray(reinterpret_cast<char*>(decrypted), sz);
|
||||
|
|
|
@ -12,6 +12,7 @@
|
|||
See the COPYING file for more details.
|
||||
*/
|
||||
|
||||
#include "toxme.h"
|
||||
#include "widget/widget.h"
|
||||
#include "misc/settings.h"
|
||||
#include "src/nexus.h"
|
||||
|
@ -20,6 +21,7 @@
|
|||
#include "src/widget/toxsave.h"
|
||||
#include "src/autoupdate.h"
|
||||
#include "src/profilelocker.h"
|
||||
#include "src/widget/loginscreen.h"
|
||||
#include <QApplication>
|
||||
#include <QCommandLineParser>
|
||||
#include <QDateTime>
|
||||
|
@ -31,9 +33,6 @@
|
|||
#include <QProcess>
|
||||
|
||||
#include <sodium.h>
|
||||
|
||||
#include "toxme.h"
|
||||
|
||||
#include <unistd.h>
|
||||
|
||||
#define EXIT_UPDATE_MACX 218 //We track our state using unique exit codes when debugging
|
||||
|
@ -292,7 +291,6 @@ int main(int argc, char *argv[])
|
|||
Nexus::getInstance().start();
|
||||
|
||||
// Run
|
||||
a.setQuitOnLastWindowClosed(false);
|
||||
int errorcode = a.exec();
|
||||
|
||||
#ifdef LOG_TO_FILE
|
||||
|
|
|
@ -1,12 +1,16 @@
|
|||
#include "nexus.h"
|
||||
#include "profile.h"
|
||||
#include "src/core/core.h"
|
||||
#include "misc/settings.h"
|
||||
#include "video/camerasource.h"
|
||||
#include "widget/gui.h"
|
||||
#include "widget/loginscreen.h"
|
||||
#include <QThread>
|
||||
#include <QDebug>
|
||||
#include <QImageReader>
|
||||
#include <QFile>
|
||||
#include <QApplication>
|
||||
#include <cassert>
|
||||
|
||||
#ifdef Q_OS_ANDROID
|
||||
#include <src/widget/androidgui.h>
|
||||
|
@ -18,18 +22,14 @@ static Nexus* nexus{nullptr};
|
|||
|
||||
Nexus::Nexus(QObject *parent) :
|
||||
QObject(parent),
|
||||
core{nullptr},
|
||||
coreThread{nullptr},
|
||||
profile{nullptr},
|
||||
widget{nullptr},
|
||||
androidgui{nullptr},
|
||||
started{false}
|
||||
androidgui{nullptr}
|
||||
{
|
||||
}
|
||||
|
||||
Nexus::~Nexus()
|
||||
{
|
||||
delete core;
|
||||
delete coreThread;
|
||||
#ifdef Q_OS_ANDROID
|
||||
delete androidgui;
|
||||
#else
|
||||
|
@ -39,9 +39,6 @@ Nexus::~Nexus()
|
|||
|
||||
void Nexus::start()
|
||||
{
|
||||
if (started)
|
||||
return;
|
||||
|
||||
qDebug() << "Starting up";
|
||||
|
||||
// Setup the environment
|
||||
|
@ -59,19 +56,30 @@ void Nexus::start()
|
|||
qRegisterMetaType<Core::PasswordType>("Core::PasswordType");
|
||||
qRegisterMetaType<std::shared_ptr<VideoFrame>>("std::shared_ptr<VideoFrame>");
|
||||
|
||||
// Create and show login screen
|
||||
loginScreen = new LoginScreen();
|
||||
showLogin();
|
||||
}
|
||||
|
||||
void Nexus::showLogin()
|
||||
{
|
||||
((QApplication*)qApp)->setQuitOnLastWindowClosed(true);
|
||||
loginScreen->reset();
|
||||
loginScreen->show();
|
||||
}
|
||||
|
||||
void Nexus::showMainGUI()
|
||||
{
|
||||
assert(profile);
|
||||
|
||||
((QApplication*)qApp)->setQuitOnLastWindowClosed(false);
|
||||
loginScreen->close();
|
||||
|
||||
// Create GUI
|
||||
#ifndef Q_OS_ANDROID
|
||||
widget = Widget::getInstance();
|
||||
#endif
|
||||
|
||||
// Create Core
|
||||
QString profilePath = Settings::getInstance().detectProfile();
|
||||
coreThread = new QThread(this);
|
||||
coreThread->setObjectName("qTox Core");
|
||||
core = new Core(coreThread, profilePath);
|
||||
core->moveToThread(coreThread);
|
||||
connect(coreThread, &QThread::started, core, &Core::start);
|
||||
|
||||
// Start GUI
|
||||
#ifdef Q_OS_ANDROID
|
||||
androidgui = new AndroidGUI;
|
||||
|
@ -88,6 +96,7 @@ void Nexus::start()
|
|||
GUI::setEnabled(false);
|
||||
|
||||
// Connections
|
||||
Core* core = profile->getCore();
|
||||
#ifdef Q_OS_ANDROID
|
||||
connect(core, &Core::connected, androidgui, &AndroidGUI::onConnected);
|
||||
connect(core, &Core::disconnected, androidgui, &AndroidGUI::onDisconnected);
|
||||
|
@ -138,10 +147,7 @@ void Nexus::start()
|
|||
connect(widget, &Widget::changeProfile, core, &Core::switchConfiguration);
|
||||
#endif
|
||||
|
||||
// Start Core
|
||||
coreThread->start();
|
||||
|
||||
started = true;
|
||||
profile->startCore();
|
||||
}
|
||||
|
||||
Nexus& Nexus::getInstance()
|
||||
|
@ -160,7 +166,20 @@ void Nexus::destroyInstance()
|
|||
|
||||
Core* Nexus::getCore()
|
||||
{
|
||||
return getInstance().core;
|
||||
Nexus& nexus = getInstance();
|
||||
if (!nexus.profile)
|
||||
return nullptr;
|
||||
return nexus.profile->getCore();
|
||||
}
|
||||
|
||||
Profile* Nexus::getProfile()
|
||||
{
|
||||
return getInstance().profile;
|
||||
}
|
||||
|
||||
void Nexus::setProfile(Profile* profile)
|
||||
{
|
||||
getInstance().profile = profile;
|
||||
}
|
||||
|
||||
AndroidGUI* Nexus::getAndroidGUI()
|
||||
|
|
18
src/nexus.h
18
src/nexus.h
|
@ -3,10 +3,11 @@
|
|||
|
||||
#include <QObject>
|
||||
|
||||
class QThread;
|
||||
class Core;
|
||||
class Widget;
|
||||
class AndroidGUI;
|
||||
class Profile;
|
||||
class LoginScreen;
|
||||
class Core;
|
||||
|
||||
/// This class is in charge of connecting various systems together
|
||||
/// and forwarding signals appropriately to the right objects
|
||||
|
@ -15,11 +16,17 @@ class Nexus : public QObject
|
|||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
void start(); ///< Will initialise the systems (GUI, Core, ...)
|
||||
void start(); ///< Sets up invariants and calls showLogin
|
||||
void showLogin(); ///< Shows the login screen
|
||||
/// Hides the login screen and shows the GUI for the given profile.
|
||||
/// Will delete the current GUI, if it exists.
|
||||
void showMainGUI();
|
||||
|
||||
static Nexus& getInstance();
|
||||
static void destroyInstance();
|
||||
static Core* getCore(); ///< Will return 0 if not started
|
||||
static Profile* getProfile(); ///< Will return 0 if not started
|
||||
static void setProfile(Profile* profile); ///< Delete the current profile, if any, and replaces it
|
||||
static AndroidGUI* getAndroidGUI(); ///< Will return 0 if not started
|
||||
static Widget* getDesktopGUI(); ///< Will return 0 if not started
|
||||
static QString getSupportedImageFilter();
|
||||
|
@ -30,11 +37,10 @@ private:
|
|||
~Nexus();
|
||||
|
||||
private:
|
||||
Core* core;
|
||||
QThread* coreThread;
|
||||
Profile* profile;
|
||||
Widget* widget;
|
||||
AndroidGUI* androidgui;
|
||||
bool started;
|
||||
LoginScreen* loginScreen;
|
||||
};
|
||||
|
||||
#endif // NEXUS_H
|
||||
|
|
116
src/profile.cpp
116
src/profile.cpp
|
@ -1,19 +1,48 @@
|
|||
#include "profile.h"
|
||||
#include "profilelocker.h"
|
||||
#include "src/misc/settings.h"
|
||||
#include "src/core/core.h"
|
||||
#include <cassert>
|
||||
#include <QDir>
|
||||
#include <QFileInfo>
|
||||
#include <QThread>
|
||||
#include <QObject>
|
||||
#include <QDebug>
|
||||
|
||||
QVector<QString> Profile::profiles;
|
||||
|
||||
Profile::Profile()
|
||||
Profile::Profile(QString name, QString password)
|
||||
: name{name}, password{password}
|
||||
{
|
||||
coreThread = new QThread();
|
||||
coreThread->setObjectName("qTox Core");
|
||||
core = new Core(coreThread, *this);
|
||||
core->moveToThread(coreThread);
|
||||
QObject::connect(coreThread, &QThread::started, core, &Core::start);
|
||||
}
|
||||
|
||||
Profile* Profile::loadProfile(QString name, QString password)
|
||||
{
|
||||
if (ProfileLocker::hasLock())
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
if (!ProfileLocker::lock(name))
|
||||
{
|
||||
qWarning() << "Failed to lock profile "<<name;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return new Profile(name, password);
|
||||
}
|
||||
|
||||
Profile::~Profile()
|
||||
{
|
||||
|
||||
delete core;
|
||||
delete coreThread;
|
||||
assert(ProfileLocker::getCurLockName() == name);
|
||||
ProfileLocker::unlock();
|
||||
}
|
||||
|
||||
QVector<QString> Profile::getFilesByExt(QString extension)
|
||||
|
@ -52,3 +81,86 @@ QVector<QString> Profile::getProfiles()
|
|||
{
|
||||
return profiles;
|
||||
}
|
||||
|
||||
Core* Profile::getCore()
|
||||
{
|
||||
return core;
|
||||
}
|
||||
|
||||
void Profile::startCore()
|
||||
{
|
||||
coreThread->start();
|
||||
}
|
||||
|
||||
QByteArray Profile::loadToxSave()
|
||||
{
|
||||
QByteArray data;
|
||||
QString path = Settings::getSettingsDirPath() + QDir::separator() + name;
|
||||
|
||||
QFile saveFile(path);
|
||||
qint64 fileSize;
|
||||
qDebug() << "Loading tox save "<<path;
|
||||
|
||||
if (!saveFile.exists())
|
||||
{
|
||||
qWarning() << "The tox save file "<<path<<" was not found";
|
||||
goto fail;
|
||||
}
|
||||
|
||||
if (!saveFile.open(QIODevice::ReadOnly))
|
||||
{
|
||||
qCritical() << "The tox save file " << path << " couldn't' be opened";
|
||||
goto fail;
|
||||
}
|
||||
|
||||
fileSize = saveFile.size();
|
||||
if (fileSize <= 0)
|
||||
{
|
||||
qWarning() << "The tox save file"<<path<<" is empty!";
|
||||
goto fail;
|
||||
}
|
||||
|
||||
data = saveFile.readAll();
|
||||
if (tox_is_data_encrypted((uint8_t*)data.data()))
|
||||
{
|
||||
if (password.isEmpty())
|
||||
{
|
||||
qCritical() << "The tox save file is encrypted, but we don't have a password!";
|
||||
goto fail;
|
||||
}
|
||||
|
||||
uint8_t salt[TOX_PASS_SALT_LENGTH];
|
||||
tox_get_salt(reinterpret_cast<uint8_t *>(data.data()), salt);
|
||||
core->setPassword(password, Core::ptMain, salt);
|
||||
|
||||
data = core->decryptData(data, Core::ptMain);
|
||||
if (data.isEmpty())
|
||||
qCritical() << "Failed to decrypt the tox save file";
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!password.isEmpty())
|
||||
qWarning() << "We have a password, but the tox save file is not encrypted";
|
||||
}
|
||||
|
||||
fail:
|
||||
saveFile.close();
|
||||
return data;
|
||||
}
|
||||
|
||||
bool Profile::isProfileEncrypted(QString name)
|
||||
{
|
||||
uint8_t data[encryptHeaderSize] = {0};
|
||||
QString path = Settings::getSettingsDirPath() + QDir::separator() + name + ".tox";
|
||||
QFile saveFile(path);
|
||||
if (!saveFile.open(QIODevice::ReadOnly))
|
||||
{
|
||||
qWarning() << "Couldn't open tox save "<<path;
|
||||
return false;
|
||||
}
|
||||
|
||||
saveFile.read((char*)data, encryptHeaderSize);
|
||||
saveFile.close();
|
||||
|
||||
return tox_is_data_encrypted(data);
|
||||
}
|
||||
|
|
|
@ -3,20 +3,34 @@
|
|||
|
||||
#include <QVector>
|
||||
#include <QString>
|
||||
#include <QByteArray>
|
||||
|
||||
class Core;
|
||||
class QThread;
|
||||
|
||||
/// Manages user profiles
|
||||
class Profile
|
||||
{
|
||||
public:
|
||||
Profile();
|
||||
/// Locks and loads an existing profile and create the associate Core* instance
|
||||
/// Returns a nullptr on error, for example if the profile is already in use
|
||||
Profile* loadProfile(QString name, QString password);
|
||||
~Profile();
|
||||
|
||||
Core* getCore();
|
||||
void startCore(); ///< Starts the Core thread
|
||||
QByteArray loadToxSave(); ///< Loads the profile's .tox save from file, unencrypted
|
||||
|
||||
/// Scan for profile, automatically importing them if needed
|
||||
/// NOT thread-safe
|
||||
static void scanProfiles();
|
||||
static QVector<QString> getProfiles();
|
||||
|
||||
/// Checks whether a profile is encrypted. Return false on error.
|
||||
static bool isProfileEncrypted(QString name);
|
||||
|
||||
private:
|
||||
Profile(QString name, QString password);
|
||||
/// Lists all the files in the config dir with a given extension
|
||||
/// Pass the raw extension, e.g. "jpeg" not ".jpeg".
|
||||
static QVector<QString> getFilesByExt(QString extension);
|
||||
|
@ -25,7 +39,13 @@ private:
|
|||
static void importProfile(QString name);
|
||||
|
||||
private:
|
||||
Core* core;
|
||||
QThread* coreThread;
|
||||
QString name, password;
|
||||
static QVector<QString> profiles;
|
||||
/// How much data we need to read to check if the file is encrypted
|
||||
/// Must be >= TOX_ENC_SAVE_MAGIC_LENGTH (8), which isn't publicly defined
|
||||
static constexpr int encryptHeaderSize = 8;
|
||||
};
|
||||
|
||||
#endif // PROFILE_H
|
||||
|
|
|
@ -97,3 +97,16 @@ void ProfileLocker::deathByBrokenLock()
|
|||
qCritical() << "Lock is *BROKEN*, exiting immediately";
|
||||
abort();
|
||||
}
|
||||
|
||||
bool ProfileLocker::hasLock()
|
||||
{
|
||||
return lockfile.operator bool();
|
||||
}
|
||||
|
||||
QString ProfileLocker::getCurLockName()
|
||||
{
|
||||
if (lockfile)
|
||||
return curLockName;
|
||||
else
|
||||
return QString();
|
||||
}
|
||||
|
|
|
@ -24,6 +24,10 @@ public:
|
|||
static bool lock(QString profile);
|
||||
/// Releases the lock on the current profile
|
||||
static void unlock();
|
||||
/// Returns true if we're currently holding a lock
|
||||
static bool hasLock();
|
||||
/// Return the name of the currently loaded profile, a null string if there is none
|
||||
static QString getCurLockName();
|
||||
/// Releases all locks on all profiles
|
||||
/// DO NOT call unless all we're the only qTox instance
|
||||
/// and we don't hold any lock yet.
|
||||
|
|
|
@ -12,7 +12,24 @@ LoginScreen::LoginScreen(QWidget *parent) :
|
|||
connect(ui->loginPgbtn, &QPushButton::clicked, this, &LoginScreen::onLoginPageClicked);
|
||||
connect(ui->createAccountButton, &QPushButton::clicked, this, &LoginScreen::onCreateNewProfile);
|
||||
connect(ui->loginButton, &QPushButton::clicked, this, &LoginScreen::onLogin);
|
||||
connect(ui->loginUsernames, &QComboBox::currentTextChanged, this, &LoginScreen::onLoginUsernameSelected);
|
||||
|
||||
reset();
|
||||
}
|
||||
|
||||
LoginScreen::~LoginScreen()
|
||||
{
|
||||
delete ui;
|
||||
}
|
||||
|
||||
void LoginScreen::reset()
|
||||
{
|
||||
ui->newUsername->clear();
|
||||
ui->newPass->clear();
|
||||
ui->loginPassword->clear();
|
||||
|
||||
ui->loginUsernames->clear();
|
||||
Profile::scanProfiles();
|
||||
QVector<QString> profiles = Profile::getProfiles();
|
||||
for (QString profile : profiles)
|
||||
ui->loginUsernames->addItem(profile);
|
||||
|
@ -23,11 +40,6 @@ LoginScreen::LoginScreen(QWidget *parent) :
|
|||
ui->stackedWidget->setCurrentIndex(1);
|
||||
}
|
||||
|
||||
LoginScreen::~LoginScreen()
|
||||
{
|
||||
delete ui;
|
||||
}
|
||||
|
||||
void LoginScreen::onNewProfilePageClicked()
|
||||
{
|
||||
ui->stackedWidget->setCurrentIndex(0);
|
||||
|
@ -43,6 +55,23 @@ void LoginScreen::onCreateNewProfile()
|
|||
|
||||
}
|
||||
|
||||
void LoginScreen::onLoginUsernameSelected(const QString &name)
|
||||
{
|
||||
if (name.isEmpty())
|
||||
return;
|
||||
|
||||
if (Profile::isProfileEncrypted(name))
|
||||
{
|
||||
ui->loginPasswordLabel->show();
|
||||
ui->loginPassword->show();
|
||||
}
|
||||
else
|
||||
{
|
||||
ui->loginPasswordLabel->hide();
|
||||
ui->loginPassword->hide();
|
||||
}
|
||||
}
|
||||
|
||||
void LoginScreen::onLogin()
|
||||
{
|
||||
|
||||
|
|
|
@ -14,8 +14,10 @@ class LoginScreen : public QWidget
|
|||
public:
|
||||
explicit LoginScreen(QWidget *parent = 0);
|
||||
~LoginScreen();
|
||||
void reset(); ///< Resets the UI, clears all fields
|
||||
|
||||
private slots:
|
||||
void onLoginUsernameSelected(const QString& name);
|
||||
// Buttons to change page
|
||||
void onNewProfilePageClicked();
|
||||
void onLoginPageClicked();
|
||||
|
|
|
@ -709,7 +709,7 @@ margin-bottom:5px;</string>
|
|||
<widget class="QComboBox" name="loginUsernames"/>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="label_4">
|
||||
<widget class="QLabel" name="loginPasswordLabel">
|
||||
<property name="text">
|
||||
<string>Password:</string>
|
||||
</property>
|
||||
|
|
Loading…
Reference in New Issue
Block a user