mirror of
https://github.com/Nheko-Reborn/nheko.git
synced 2024-11-22 19:08:58 +03:00
Calculate verification status from cross-signing sigs and update dynamically
This commit is contained in:
parent
64d5a193f1
commit
7b6fab3373
9 changed files with 223 additions and 157 deletions
|
@ -56,7 +56,7 @@ ApplicationWindow{
|
|||
|
||||
Button {
|
||||
id: verifyUserButton
|
||||
text: "Verify"
|
||||
text: qsTr("Verify")
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
enabled: !profile.isUserVerified
|
||||
visible: !profile.isUserVerified
|
||||
|
@ -155,7 +155,6 @@ ApplicationWindow{
|
|||
onClicked: {
|
||||
if(model.verificationStatus == VerificationStatus.VERIFIED){
|
||||
profile.unverify(model.deviceId)
|
||||
deviceVerificationList.updateProfile(newFlow.userId);
|
||||
}else{
|
||||
profile.verify(model.deviceId);
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import QtQuick 2.3
|
||||
import QtQuick 2.10
|
||||
import QtQuick.Controls 2.10
|
||||
import QtQuick.Window 2.2
|
||||
import QtQuick.Window 2.10
|
||||
|
||||
import im.nheko 1.0
|
||||
|
||||
|
|
172
src/Cache.cpp
172
src/Cache.cpp
|
@ -3184,6 +3184,28 @@ Cache::updateUserKeys(const std::string &sync_token, const mtx::responses::Query
|
|||
}
|
||||
|
||||
txn.commit();
|
||||
|
||||
std::map<std::string, VerificationStatus> tmp;
|
||||
const auto local_user = utils::localUser().toStdString();
|
||||
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(verification_storage.verification_storage_mtx);
|
||||
for (auto &[user_id, update] : updates) {
|
||||
if (user_id == local_user) {
|
||||
std::swap(tmp, verification_storage.status);
|
||||
} else {
|
||||
verification_storage.status.erase(user_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (auto &[user_id, update] : updates) {
|
||||
if (user_id == local_user) {
|
||||
for (const auto &[user, status] : tmp)
|
||||
emit verificationStatusChanged(user);
|
||||
} else {
|
||||
emit verificationStatusChanged(user_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
|
@ -3236,8 +3258,6 @@ Cache::markUserKeysOutOfDate(lmdb::txn &txn,
|
|||
void
|
||||
to_json(json &j, const VerificationCache &info)
|
||||
{
|
||||
j["verified_master_key"] = info.verified_master_key;
|
||||
j["cross_verified"] = info.cross_verified;
|
||||
j["device_verified"] = info.device_verified;
|
||||
j["device_blocked"] = info.device_blocked;
|
||||
}
|
||||
|
@ -3245,14 +3265,12 @@ to_json(json &j, const VerificationCache &info)
|
|||
void
|
||||
from_json(const json &j, VerificationCache &info)
|
||||
{
|
||||
info.verified_master_key = j.at("verified_master_key");
|
||||
info.cross_verified = j.at("cross_verified").get<std::vector<std::string>>();
|
||||
info.device_verified = j.at("device_verified").get<std::vector<std::string>>();
|
||||
info.device_blocked = j.at("device_blocked").get<std::vector<std::string>>();
|
||||
}
|
||||
|
||||
std::optional<VerificationCache>
|
||||
Cache::verificationStatus(const std::string &user_id)
|
||||
Cache::verificationCache(const std::string &user_id)
|
||||
{
|
||||
lmdb::val verifiedVal;
|
||||
|
||||
|
@ -3298,6 +3316,23 @@ Cache::markDeviceVerified(const std::string &user_id, const std::string &key)
|
|||
txn.commit();
|
||||
} catch (std::exception &) {
|
||||
}
|
||||
|
||||
const auto local_user = utils::localUser().toStdString();
|
||||
std::map<std::string, VerificationStatus> tmp;
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(verification_storage.verification_storage_mtx);
|
||||
if (user_id == local_user) {
|
||||
std::swap(tmp, verification_storage.status);
|
||||
} else {
|
||||
verification_storage.status.erase(user_id);
|
||||
}
|
||||
}
|
||||
if (user_id == local_user) {
|
||||
for (const auto &[user, status] : tmp)
|
||||
emit verificationStatusChanged(user);
|
||||
} else {
|
||||
emit verificationStatusChanged(user_id);
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
|
@ -3325,27 +3360,112 @@ Cache::markDeviceUnverified(const std::string &user_id, const std::string &key)
|
|||
txn.commit();
|
||||
} catch (std::exception &) {
|
||||
}
|
||||
|
||||
const auto local_user = utils::localUser().toStdString();
|
||||
std::map<std::string, VerificationStatus> tmp;
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(verification_storage.verification_storage_mtx);
|
||||
if (user_id == local_user) {
|
||||
std::swap(tmp, verification_storage.status);
|
||||
} else {
|
||||
verification_storage.status.erase(user_id);
|
||||
}
|
||||
}
|
||||
if (user_id == local_user) {
|
||||
for (const auto &[user, status] : tmp)
|
||||
emit verificationStatusChanged(user);
|
||||
} else {
|
||||
emit verificationStatusChanged(user_id);
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
Cache::markMasterKeyVerified(const std::string &user_id, const std::string &key)
|
||||
VerificationStatus
|
||||
Cache::verificationStatus(const std::string &user_id)
|
||||
{
|
||||
lmdb::val val;
|
||||
std::unique_lock<std::mutex> lock(verification_storage.verification_storage_mtx);
|
||||
if (verification_storage.status.count(user_id))
|
||||
return verification_storage.status.at(user_id);
|
||||
|
||||
auto txn = lmdb::txn::begin(env_);
|
||||
auto db = getVerificationDb(txn);
|
||||
VerificationStatus status;
|
||||
|
||||
try {
|
||||
VerificationCache verified_state;
|
||||
auto res = lmdb::dbi_get(txn, db, lmdb::val(user_id), val);
|
||||
if (res) {
|
||||
verified_state = json::parse(std::string_view(val.data(), val.size()));
|
||||
if (auto verifCache = verificationCache(user_id)) {
|
||||
status.verified_devices = verifCache->device_verified;
|
||||
}
|
||||
|
||||
verified_state.verified_master_key = key;
|
||||
lmdb::dbi_put(txn, db, lmdb::val(user_id), lmdb::val(json(verified_state).dump()));
|
||||
txn.commit();
|
||||
const auto local_user = utils::localUser().toStdString();
|
||||
|
||||
if (user_id == local_user)
|
||||
status.verified_devices.push_back(http::client()->device_id());
|
||||
|
||||
verification_storage.status[user_id] = status;
|
||||
|
||||
auto verifyAtLeastOneSig = [](const auto &toVerif,
|
||||
const std::map<std::string, std::string> &keys,
|
||||
const std::string &keyOwner) {
|
||||
if (!toVerif.signatures.count(keyOwner))
|
||||
return false;
|
||||
|
||||
for (const auto &[key_id, signature] : toVerif.signatures.at(keyOwner)) {
|
||||
if (!keys.count(key_id))
|
||||
continue;
|
||||
|
||||
if (mtx::crypto::ed25519_verify_signature(
|
||||
keys.at(key_id), json(toVerif), signature))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
try {
|
||||
// for local user verify this device_key -> our master_key -> our self_signing_key
|
||||
// -> our device_keys
|
||||
//
|
||||
// for other user verify this device_key -> our master_key -> our user_signing_key
|
||||
// -> their master_key -> their self_signing_key -> their device_keys
|
||||
//
|
||||
// This means verifying the other user adds 2 extra steps,verifying our user_signing
|
||||
// key and their master key
|
||||
auto ourKeys = userKeys(local_user);
|
||||
auto theirKeys = userKeys(user_id);
|
||||
if (!ourKeys || !theirKeys)
|
||||
return status;
|
||||
|
||||
if (!mtx::crypto::ed25519_verify_signature(
|
||||
olm::client()->identity_keys().ed25519,
|
||||
json(ourKeys->master_keys),
|
||||
ourKeys->master_keys.signatures.at(local_user)
|
||||
.at("ed25519:" + http::client()->device_id())))
|
||||
return status;
|
||||
|
||||
auto master_keys = ourKeys->master_keys.keys;
|
||||
|
||||
if (user_id != local_user) {
|
||||
if (!verifyAtLeastOneSig(
|
||||
ourKeys->user_signing_keys, master_keys, local_user))
|
||||
return status;
|
||||
|
||||
if (!verifyAtLeastOneSig(
|
||||
theirKeys->master_keys, ourKeys->user_signing_keys.keys, local_user))
|
||||
return status;
|
||||
|
||||
master_keys = theirKeys->master_keys.keys;
|
||||
}
|
||||
|
||||
status.user_verified = true;
|
||||
|
||||
if (!verifyAtLeastOneSig(theirKeys->self_signing_keys, master_keys, user_id))
|
||||
return status;
|
||||
|
||||
for (const auto &[device, device_key] : theirKeys->device_keys) {
|
||||
if (verifyAtLeastOneSig(
|
||||
device_key, theirKeys->self_signing_keys.keys, user_id))
|
||||
status.verified_devices.push_back(device_key.device_id);
|
||||
}
|
||||
|
||||
verification_storage.status[user_id] = status;
|
||||
return status;
|
||||
} catch (std::exception &) {
|
||||
return status;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -3551,28 +3671,22 @@ updateUserKeys(const std::string &sync_token, const mtx::responses::QueryKeys &k
|
|||
}
|
||||
|
||||
// device & user verification cache
|
||||
std::optional<VerificationCache>
|
||||
std::optional<VerificationStatus>
|
||||
verificationStatus(const std::string &user_id)
|
||||
{
|
||||
return instance_->verificationStatus(user_id);
|
||||
}
|
||||
|
||||
void
|
||||
markDeviceVerified(const std::string &user_id, const std::string &key)
|
||||
markDeviceVerified(const std::string &user_id, const std::string &device)
|
||||
{
|
||||
instance_->markDeviceVerified(user_id, key);
|
||||
instance_->markDeviceVerified(user_id, device);
|
||||
}
|
||||
|
||||
void
|
||||
markDeviceUnverified(const std::string &user_id, const std::string &key)
|
||||
markDeviceUnverified(const std::string &user_id, const std::string &device)
|
||||
{
|
||||
instance_->markDeviceUnverified(user_id, key);
|
||||
}
|
||||
|
||||
void
|
||||
markMasterKeyVerified(const std::string &user_id, const std::string &key)
|
||||
{
|
||||
instance_->markMasterKeyVerified(user_id, key);
|
||||
instance_->markDeviceUnverified(user_id, device);
|
||||
}
|
||||
|
||||
std::vector<std::string>
|
||||
|
|
|
@ -67,14 +67,12 @@ void
|
|||
updateUserKeys(const std::string &sync_token, const mtx::responses::QueryKeys &keyQuery);
|
||||
|
||||
// device & user verification cache
|
||||
std::optional<VerificationCache>
|
||||
std::optional<VerificationStatus>
|
||||
verificationStatus(const std::string &user_id);
|
||||
void
|
||||
markDeviceVerified(const std::string &user_id, const std::string &key);
|
||||
markDeviceVerified(const std::string &user_id, const std::string &device);
|
||||
void
|
||||
markDeviceUnverified(const std::string &user_id, const std::string &key);
|
||||
void
|
||||
markMasterKeyVerified(const std::string &user_id, const std::string &key);
|
||||
markDeviceUnverified(const std::string &user_id, const std::string &device);
|
||||
|
||||
//! Load saved data for the display names & avatars.
|
||||
void
|
||||
|
|
|
@ -66,6 +66,23 @@ struct OlmSessionStorage
|
|||
std::mutex group_inbound_mtx;
|
||||
};
|
||||
|
||||
//! Verification status of a single user
|
||||
struct VerificationStatus
|
||||
{
|
||||
//! True, if the users master key is verified
|
||||
bool user_verified = false;
|
||||
//! List of all devices marked as verified
|
||||
std::vector<std::string> verified_devices;
|
||||
};
|
||||
|
||||
//! In memory cache of verification status
|
||||
struct VerificationStorage
|
||||
{
|
||||
//! mapping of user to verification status
|
||||
std::map<std::string, VerificationStatus> status;
|
||||
std::mutex verification_storage_mtx;
|
||||
};
|
||||
|
||||
// this will store the keys of the user with whom a encrypted room is shared with
|
||||
struct UserKeyCache
|
||||
{
|
||||
|
@ -90,12 +107,8 @@ struct VerificationCache
|
|||
{
|
||||
//! list of verified device_ids with device-verification
|
||||
std::vector<std::string> device_verified;
|
||||
//! list of verified device_ids with cross-signing, calculated from master key
|
||||
std::vector<std::string> cross_verified;
|
||||
//! list of devices the user blocks
|
||||
std::vector<std::string> device_blocked;
|
||||
//! The verified master key.
|
||||
std::string verified_master_key;
|
||||
};
|
||||
|
||||
void
|
||||
|
|
|
@ -67,10 +67,9 @@ public:
|
|||
const std::vector<std::string> &user_ids);
|
||||
|
||||
// device & user verification cache
|
||||
std::optional<VerificationCache> verificationStatus(const std::string &user_id);
|
||||
void markDeviceVerified(const std::string &user_id, const std::string &key);
|
||||
void markDeviceUnverified(const std::string &user_id, const std::string &key);
|
||||
void markMasterKeyVerified(const std::string &user_id, const std::string &key);
|
||||
VerificationStatus verificationStatus(const std::string &user_id);
|
||||
void markDeviceVerified(const std::string &user_id, const std::string &device);
|
||||
void markDeviceUnverified(const std::string &user_id, const std::string &device);
|
||||
|
||||
static void removeDisplayName(const QString &room_id, const QString &user_id);
|
||||
static void removeAvatarUrl(const QString &room_id, const QString &user_id);
|
||||
|
@ -283,6 +282,7 @@ signals:
|
|||
void removeNotification(const QString &room_id, const QString &event_id);
|
||||
void userKeysUpdate(const std::string &sync_token,
|
||||
const mtx::responses::QueryKeys &keyQuery);
|
||||
void verificationStatusChanged(const std::string &userid);
|
||||
|
||||
private:
|
||||
//! Save an invited room.
|
||||
|
@ -576,6 +576,8 @@ private:
|
|||
return QString::fromStdString(event.state_key);
|
||||
}
|
||||
|
||||
std::optional<VerificationCache> verificationCache(const std::string &user_id);
|
||||
|
||||
void setNextBatchToken(lmdb::txn &txn, const std::string &token);
|
||||
void setNextBatchToken(lmdb::txn &txn, const QString &token);
|
||||
|
||||
|
@ -600,6 +602,7 @@ private:
|
|||
static QHash<QString, QString> AvatarUrls;
|
||||
|
||||
OlmSessionStorage session_storage;
|
||||
VerificationStorage verification_storage;
|
||||
};
|
||||
|
||||
namespace cache {
|
||||
|
|
|
@ -1031,7 +1031,7 @@ TimelineModel::sendEncryptedMessage(mtx::events::RoomEvent<T> msg, mtx::events::
|
|||
|
||||
try {
|
||||
if (!mtx::crypto::verify_identity_signature(
|
||||
json(dev.second), device_id, user_id)) {
|
||||
dev.second, device_id, user_id)) {
|
||||
nhlog::crypto()->warn(
|
||||
"failed to verify identity keys: {}",
|
||||
json(dev.second).dump(2));
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
#include "UserProfile.h"
|
||||
#include "Cache.h"
|
||||
#include "Cache_p.h"
|
||||
#include "ChatPage.h"
|
||||
#include "DeviceVerificationFlow.h"
|
||||
#include "Logging.h"
|
||||
|
@ -8,8 +8,6 @@
|
|||
#include "timeline/TimelineModel.h"
|
||||
#include "timeline/TimelineViewManager.h"
|
||||
|
||||
#include <iostream> // only for debugging
|
||||
|
||||
UserProfile::UserProfile(QString roomid,
|
||||
QString userid,
|
||||
TimelineViewManager *manager_,
|
||||
|
@ -21,6 +19,31 @@ UserProfile::UserProfile(QString roomid,
|
|||
, model(parent)
|
||||
{
|
||||
fetchDeviceList(this->userid_);
|
||||
|
||||
connect(cache::client(),
|
||||
&Cache::verificationStatusChanged,
|
||||
this,
|
||||
[this](const std::string &user_id) {
|
||||
if (user_id != this->userid_.toStdString())
|
||||
return;
|
||||
|
||||
auto status = cache::verificationStatus(user_id);
|
||||
if (!status)
|
||||
return;
|
||||
this->isUserVerified = status->user_verified;
|
||||
emit userStatusChanged();
|
||||
|
||||
for (auto &deviceInfo : deviceList_.deviceList_) {
|
||||
deviceInfo.verification_status =
|
||||
std::find(status->verified_devices.begin(),
|
||||
status->verified_devices.end(),
|
||||
deviceInfo.device_id.toStdString()) ==
|
||||
status->verified_devices.end()
|
||||
? verification::UNVERIFIED
|
||||
: verification::VERIFIED;
|
||||
}
|
||||
deviceList_.reset(deviceList_.deviceList_);
|
||||
});
|
||||
}
|
||||
|
||||
QHash<int, QByteArray>
|
||||
|
@ -127,106 +150,26 @@ UserProfile::fetchDeviceList(const QString &userID)
|
|||
|
||||
std::vector<DeviceInfo> deviceInfo;
|
||||
auto devices = other_user_keys.device_keys;
|
||||
auto device_verified = cache::verificationStatus(other_user_id);
|
||||
auto verificationStatus =
|
||||
cache::client()->verificationStatus(other_user_id);
|
||||
|
||||
if (device_verified.has_value()) {
|
||||
// TODO: properly check cross-signing signatures here
|
||||
isUserVerified = !device_verified->verified_master_key.empty();
|
||||
}
|
||||
|
||||
std::optional<crypto::CrossSigningKeys> lmk, lsk, luk, mk, sk, uk;
|
||||
|
||||
lmk = res.master_keys;
|
||||
luk = res.user_signing_keys;
|
||||
lsk = res.self_signing_keys;
|
||||
mk = other_user_keys.master_keys;
|
||||
uk = other_user_keys.user_signing_keys;
|
||||
sk = other_user_keys.self_signing_keys;
|
||||
|
||||
// First checking if the user is verified
|
||||
if (luk.has_value() && mk.has_value()) {
|
||||
// iterating through the public key of local user_signing keys
|
||||
for (auto sign_key : luk.value().keys) {
|
||||
// checking if the signatures are empty as "at" could
|
||||
// cause exceptions
|
||||
auto signs = mk->signatures;
|
||||
if (!signs.empty() &&
|
||||
signs.find(local_user_id) != signs.end()) {
|
||||
auto sign = signs.at(local_user_id);
|
||||
try {
|
||||
isUserVerified =
|
||||
isUserVerified ||
|
||||
(olm::client()->ed25519_verify_sig(
|
||||
sign_key.second,
|
||||
json(mk.value()),
|
||||
sign.at(sign_key.first)));
|
||||
} catch (std::out_of_range &) {
|
||||
isUserVerified =
|
||||
isUserVerified || false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
isUserVerified = verificationStatus.user_verified;
|
||||
emit userStatusChanged();
|
||||
|
||||
for (const auto &d : devices) {
|
||||
auto device = d.second;
|
||||
verification::Status verified =
|
||||
verification::Status::UNVERIFIED;
|
||||
|
||||
if (device_verified.has_value()) {
|
||||
if (std::find(device_verified->cross_verified.begin(),
|
||||
device_verified->cross_verified.end(),
|
||||
d.first) !=
|
||||
device_verified->cross_verified.end())
|
||||
if (std::find(verificationStatus.verified_devices.begin(),
|
||||
verificationStatus.verified_devices.end(),
|
||||
device.device_id) !=
|
||||
verificationStatus.verified_devices.end() &&
|
||||
mtx::crypto::verify_identity_signature(
|
||||
device,
|
||||
DeviceId(device.device_id),
|
||||
UserId(other_user_id)))
|
||||
verified = verification::Status::VERIFIED;
|
||||
if (std::find(device_verified->device_verified.begin(),
|
||||
device_verified->device_verified.end(),
|
||||
d.first) !=
|
||||
device_verified->device_verified.end())
|
||||
verified = verification::Status::VERIFIED;
|
||||
if (std::find(device_verified->device_blocked.begin(),
|
||||
device_verified->device_blocked.end(),
|
||||
d.first) !=
|
||||
device_verified->device_blocked.end())
|
||||
verified = verification::Status::BLOCKED;
|
||||
} else if (isUserVerified) {
|
||||
device_verified = VerificationCache{};
|
||||
}
|
||||
|
||||
// won't check for already verified devices
|
||||
if (verified != verification::Status::VERIFIED &&
|
||||
isUserVerified) {
|
||||
if ((sk.has_value()) && (!device.signatures.empty())) {
|
||||
for (auto sign_key : sk.value().keys) {
|
||||
auto signs =
|
||||
device.signatures.at(other_user_id);
|
||||
try {
|
||||
if (olm::client()
|
||||
->ed25519_verify_sig(
|
||||
sign_key.second,
|
||||
json(device),
|
||||
signs.at(
|
||||
sign_key.first))) {
|
||||
verified =
|
||||
verification::Status::
|
||||
VERIFIED;
|
||||
device_verified.value()
|
||||
.cross_verified
|
||||
.push_back(d.first);
|
||||
}
|
||||
} catch (std::out_of_range &) {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO(Nico): properly show cross-signing
|
||||
// if (device_verified.has_value()) {
|
||||
// device_verified.value().is_user_verified =
|
||||
// isUserVerified;
|
||||
// cache::setVerifiedCache(user_id,
|
||||
// device_verified.value());
|
||||
//}
|
||||
|
||||
deviceInfo.push_back(
|
||||
{QString::fromStdString(d.first),
|
||||
|
@ -235,14 +178,6 @@ UserProfile::fetchDeviceList(const QString &userID)
|
|||
verified});
|
||||
}
|
||||
|
||||
std::cout << (isUserVerified ? "Yes" : "No") << std::endl;
|
||||
|
||||
std::sort(deviceInfo.begin(),
|
||||
deviceInfo.end(),
|
||||
[](const DeviceInfo &a, const DeviceInfo &b) {
|
||||
return a.device_id > b.device_id;
|
||||
});
|
||||
|
||||
this->deviceList_.queueReset(std::move(deviceInfo));
|
||||
});
|
||||
});
|
||||
|
|
|
@ -74,6 +74,8 @@ public slots:
|
|||
|
||||
private:
|
||||
std::vector<DeviceInfo> deviceList_;
|
||||
|
||||
friend class UserProfile;
|
||||
};
|
||||
|
||||
class UserProfile : public QObject
|
||||
|
@ -83,7 +85,7 @@ class UserProfile : public QObject
|
|||
Q_PROPERTY(QString userid READ userid CONSTANT)
|
||||
Q_PROPERTY(QString avatarUrl READ avatarUrl CONSTANT)
|
||||
Q_PROPERTY(DeviceInfoModel *deviceList READ deviceList CONSTANT)
|
||||
Q_PROPERTY(bool isUserVerified READ getUserStatus CONSTANT)
|
||||
Q_PROPERTY(bool isUserVerified READ getUserStatus NOTIFY userStatusChanged)
|
||||
public:
|
||||
UserProfile(QString roomid,
|
||||
QString userid,
|
||||
|
@ -105,9 +107,11 @@ public:
|
|||
Q_INVOKABLE void kickUser();
|
||||
Q_INVOKABLE void startChat();
|
||||
|
||||
signals:
|
||||
void userStatusChanged();
|
||||
|
||||
private:
|
||||
QString roomid_, userid_;
|
||||
std::optional<std::string> cross_verified;
|
||||
DeviceInfoModel deviceList_;
|
||||
bool isUserVerified = false;
|
||||
TimelineViewManager *manager;
|
||||
|
|
Loading…
Reference in a new issue