mirror of
https://github.com/swift-project/pilotclient.git
synced 2026-04-20 04:25:42 +08:00
AFV initial commit
This commit is contained in:
committed by
Mat Sutcliffe
parent
7030302e73
commit
b5a2f2ad13
148
src/blackcore/afv/connection/apiserverconnection.cpp
Normal file
148
src/blackcore/afv/connection/apiserverconnection.cpp
Normal file
@@ -0,0 +1,148 @@
|
||||
#include "apiserverconnection.h"
|
||||
#include <QJsonObject>
|
||||
#include <QJsonArray>
|
||||
#include <QUrl>
|
||||
#include <QUrlQuery>
|
||||
|
||||
ApiServerConnection::ApiServerConnection(const QString &address, QObject *parent) :
|
||||
QObject(parent),
|
||||
m_address(address)
|
||||
{
|
||||
qDebug() << "ApiServerConnection instantiated";
|
||||
}
|
||||
|
||||
void ApiServerConnection::connectTo(const QString &username, const QString &password, const QUuid &networkVersion)
|
||||
{
|
||||
m_username = username;
|
||||
m_password = password;
|
||||
m_networkVersion = networkVersion;
|
||||
m_watch.start();
|
||||
|
||||
QUrl url(m_address);
|
||||
url.setPath("/api/v1/auth");
|
||||
|
||||
QJsonObject obj
|
||||
{
|
||||
{"username", username},
|
||||
{"password", password},
|
||||
{"networkversion", networkVersion.toString()},
|
||||
};
|
||||
|
||||
QNetworkAccessManager *nam = sApp->getNetworkAccessManager();
|
||||
QEventLoop loop;
|
||||
connect(nam, &QNetworkAccessManager::finished, &loop, &QEventLoop::quit);
|
||||
QNetworkRequest request(url);
|
||||
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
|
||||
QNetworkReply *reply = nam->post(request, QJsonDocument(obj).toJson());
|
||||
while(! reply->isFinished() ) { loop.exec(); }
|
||||
qDebug() << "POST api/v1/auth (" << m_watch.elapsed() << "ms)";
|
||||
|
||||
if (reply->error() != QNetworkReply::NoError)
|
||||
{
|
||||
qWarning() << reply->errorString();
|
||||
return;
|
||||
}
|
||||
|
||||
m_jwt = reply->readAll().trimmed();
|
||||
// TODO JwtSecurityToken. Now we assume its 6 hours
|
||||
m_serverToUserOffset = 0;
|
||||
m_expiryLocalUtc = QDateTime::currentDateTimeUtc().addSecs( 6 * 60 * 60);
|
||||
m_isAuthenticated = true;
|
||||
}
|
||||
|
||||
PostCallsignResponseDto ApiServerConnection::addCallsign(const QString &callsign)
|
||||
{
|
||||
return postNoRequest<PostCallsignResponseDto>("/api/v1/users/" + m_username + "/callsigns/" + callsign);
|
||||
}
|
||||
|
||||
void ApiServerConnection::removeCallsign(const QString &callsign)
|
||||
{
|
||||
deleteResource("/api/v1/users/" + m_username + "/callsigns/" + callsign);
|
||||
}
|
||||
|
||||
void ApiServerConnection::updateTransceivers(const QString &callsign, const QVector<TransceiverDto> &transceivers)
|
||||
{
|
||||
QJsonArray array;
|
||||
for (const TransceiverDto &tx : transceivers)
|
||||
{
|
||||
array.append(tx.toJson());
|
||||
}
|
||||
|
||||
postNoResponse("/api/v1/users/" + m_username + "/callsigns/" + callsign + "/transceivers", QJsonDocument(array));
|
||||
}
|
||||
|
||||
void ApiServerConnection::forceDisconnect()
|
||||
{
|
||||
m_isAuthenticated = false;
|
||||
m_jwt.clear();
|
||||
}
|
||||
|
||||
void ApiServerConnection::postNoResponse(const QString &resource, const QJsonDocument &json)
|
||||
{
|
||||
if (! m_isAuthenticated)
|
||||
{
|
||||
qDebug() << "Not authenticated";
|
||||
return;
|
||||
}
|
||||
|
||||
checkExpiry();
|
||||
|
||||
m_watch.start();
|
||||
QUrl url(m_address);
|
||||
url.setPath(resource);
|
||||
QNetworkAccessManager *nam = sApp->getNetworkAccessManager();
|
||||
QEventLoop loop;
|
||||
connect(nam, &QNetworkAccessManager::finished, &loop, &QEventLoop::quit);
|
||||
|
||||
QNetworkRequest request(url);
|
||||
request.setRawHeader("Authorization", "Bearer " + m_jwt);
|
||||
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
|
||||
QNetworkReply *reply = nam->post(request, json.toJson());
|
||||
while(! reply->isFinished() ) { loop.exec(); }
|
||||
qDebug() << "POST" << resource << "(" << m_watch.elapsed() << "ms)";
|
||||
|
||||
if (reply->error() != QNetworkReply::NoError)
|
||||
{
|
||||
qWarning() << "POST" << resource << "failed:" << reply->errorString();
|
||||
return;
|
||||
}
|
||||
|
||||
reply->deleteLater();
|
||||
}
|
||||
|
||||
void ApiServerConnection::deleteResource(const QString &resource)
|
||||
{
|
||||
if (! m_isAuthenticated) { return; }
|
||||
|
||||
m_watch.start();
|
||||
QUrl url(m_address);
|
||||
url.setPath(resource);
|
||||
|
||||
QNetworkAccessManager *nam = sApp->getNetworkAccessManager();
|
||||
QEventLoop loop;
|
||||
connect(nam, &QNetworkAccessManager::finished, &loop, &QEventLoop::quit);
|
||||
|
||||
QNetworkRequest request(url);
|
||||
request.setRawHeader("Authorization", "Bearer " + m_jwt);
|
||||
QNetworkReply *reply = nam->deleteResource(request);
|
||||
while(! reply->isFinished() ) { loop.exec(); }
|
||||
qDebug() << "DELETE" << resource << "(" << m_watch.elapsed() << "ms)";
|
||||
|
||||
if (reply->error() != QNetworkReply::NoError)
|
||||
{
|
||||
qWarning() << "DELETE" << resource << "failed:" << reply->errorString();
|
||||
return;
|
||||
}
|
||||
|
||||
reply->deleteLater();
|
||||
}
|
||||
|
||||
void ApiServerConnection::checkExpiry()
|
||||
{
|
||||
if (QDateTime::currentDateTimeUtc() > m_expiryLocalUtc.addSecs(-5 * 60))
|
||||
{
|
||||
connectTo(m_username, m_password, m_networkVersion);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
99
src/blackcore/afv/connection/apiserverconnection.h
Normal file
99
src/blackcore/afv/connection/apiserverconnection.h
Normal file
@@ -0,0 +1,99 @@
|
||||
#ifndef APISERVERCONNECTION_H
|
||||
#define APISERVERCONNECTION_H
|
||||
|
||||
#include "blackcore/afv/dto.h"
|
||||
#include "blackcore/application.h"
|
||||
|
||||
#include <QString>
|
||||
#include <QNetworkAccessManager>
|
||||
#include <QObject>
|
||||
#include <QElapsedTimer>
|
||||
#include <QUuid>
|
||||
#include <QDebug>
|
||||
#include <QEventLoop>
|
||||
#include <QNetworkRequest>
|
||||
#include <QNetworkReply>
|
||||
#include <QJsonDocument>
|
||||
|
||||
// TODO:
|
||||
// - JWT refresh
|
||||
|
||||
class ApiServerConnection : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
enum ServerError
|
||||
{
|
||||
NoError
|
||||
};
|
||||
|
||||
ApiServerConnection(const QString &address, QObject *parent = nullptr);
|
||||
|
||||
bool isAuthenticated() const { return m_isAuthenticated; }
|
||||
void connectTo(const QString &username, const QString &password, const QUuid &networkVersion);
|
||||
|
||||
PostCallsignResponseDto addCallsign(const QString &callsign);
|
||||
void removeCallsign(const QString &callsign);
|
||||
|
||||
void updateTransceivers(const QString &callsign, const QVector<TransceiverDto> &transceivers);
|
||||
|
||||
void forceDisconnect();
|
||||
|
||||
private:
|
||||
template<typename TResponse>
|
||||
TResponse postNoRequest(const QString &resource)
|
||||
{
|
||||
if (! m_isAuthenticated)
|
||||
{
|
||||
qDebug() << "Not authenticated";
|
||||
return {};
|
||||
}
|
||||
|
||||
checkExpiry();
|
||||
|
||||
QNetworkAccessManager *nam = sApp->getNetworkAccessManager();
|
||||
|
||||
m_watch.start();
|
||||
QUrl url(m_address);
|
||||
url.setPath(resource);
|
||||
QEventLoop loop;
|
||||
connect(nam, &QNetworkAccessManager::finished, &loop, &QEventLoop::quit);
|
||||
|
||||
QNetworkRequest request(url);
|
||||
request.setRawHeader("Authorization", "Bearer " + m_jwt);
|
||||
QNetworkReply *reply = nam->post(request, QByteArray());
|
||||
while(! reply->isFinished() ) { loop.exec(); }
|
||||
qDebug() << "POST" << resource << "(" << m_watch.elapsed() << "ms)";
|
||||
|
||||
if (reply->error() != QNetworkReply::NoError)
|
||||
{
|
||||
qWarning() << "POST" << resource << "failed:" << reply->errorString();
|
||||
return {};
|
||||
}
|
||||
|
||||
const QJsonDocument doc = QJsonDocument::fromJson(reply->readAll());
|
||||
TResponse response = TResponse::fromJson(doc.object());
|
||||
|
||||
reply->deleteLater();
|
||||
return response;
|
||||
}
|
||||
|
||||
void postNoResponse(const QString &resource, const QJsonDocument &json);
|
||||
void deleteResource(const QString &resource);
|
||||
void checkExpiry();
|
||||
|
||||
const QString m_address;
|
||||
QByteArray m_jwt;
|
||||
QString m_username;
|
||||
QString m_password;
|
||||
QUuid m_networkVersion;
|
||||
QDateTime m_expiryLocalUtc;
|
||||
qint64 m_serverToUserOffset;
|
||||
|
||||
bool m_isAuthenticated = false;
|
||||
|
||||
QElapsedTimer m_watch;
|
||||
};
|
||||
|
||||
#endif // APISERVERCONNECTION_H
|
||||
146
src/blackcore/afv/connection/clientconnection.cpp
Normal file
146
src/blackcore/afv/connection/clientconnection.cpp
Normal file
@@ -0,0 +1,146 @@
|
||||
#include "clientconnection.h"
|
||||
#include <QNetworkDatagram>
|
||||
|
||||
ClientConnection::ClientConnection(const QString &apiServer, QObject *parent) :
|
||||
QObject(parent),
|
||||
m_apiServerConnection(apiServer, this)
|
||||
{
|
||||
qDebug() << "ClientConnection instantiated";
|
||||
|
||||
// connect(&m_apiServerConnection, &ApiServerConnection::authenticationFinished, this, &ClientConnection::apiConnectionFinished);
|
||||
// connect(&m_apiServerConnection, &ApiServerConnection::addCallsignFinished, this, &ClientConnection::addCallsignFinished);
|
||||
// connect(&m_apiServerConnection, &ApiServerConnection::removeCallsignFinished, this, &ClientConnection::removeCallsignFinished);
|
||||
|
||||
connect(&m_voiceServerTimer, &QTimer::timeout, this, &ClientConnection::voiceServerHeartbeat);
|
||||
|
||||
connect(&m_udpSocket, &QUdpSocket::readyRead, this, &ClientConnection::readPendingDatagrams);
|
||||
connect(&m_udpSocket, qOverload<QAbstractSocket::SocketError>(&QUdpSocket::error), this, &ClientConnection::handleSocketError);
|
||||
}
|
||||
|
||||
void ClientConnection::connectTo(const QString &userName, const QString &password, const QString &callsign)
|
||||
{
|
||||
if (m_connection.m_connected)
|
||||
{
|
||||
qDebug() << "Client already connected";
|
||||
return;
|
||||
}
|
||||
|
||||
m_connection.m_userName = userName;
|
||||
m_connection.m_callsign = callsign;
|
||||
m_apiServerConnection.connectTo(userName, password, m_networkVersion);
|
||||
m_connection.m_tokens = m_apiServerConnection.addCallsign(m_connection.m_callsign);
|
||||
m_connection.m_authenticatedDateTimeUtc = QDateTime::currentDateTimeUtc();
|
||||
m_connection.createCryptoChannels();
|
||||
|
||||
connectToVoiceServer();
|
||||
|
||||
// taskServerConnectionCheck.Start();
|
||||
|
||||
m_connection.m_connected = true;
|
||||
qDebug() << "Connected:" << callsign;
|
||||
}
|
||||
|
||||
void ClientConnection::disconnectFrom(const QString &reason)
|
||||
{
|
||||
if (! m_connection.m_connected)
|
||||
{
|
||||
qDebug() << "Client not connected";
|
||||
return;
|
||||
}
|
||||
|
||||
m_connection.m_connected = false;
|
||||
// TODO emit disconnected(reason)
|
||||
qDebug() << "Disconnected:" << reason;
|
||||
|
||||
if (! m_connection.m_callsign.isEmpty())
|
||||
{
|
||||
m_apiServerConnection.removeCallsign(m_connection.m_callsign);
|
||||
}
|
||||
|
||||
// TODO connectionCheckCancelTokenSource.Cancel(); //Stops connection check loop
|
||||
disconnectFromVoiceServer();
|
||||
m_apiServerConnection.forceDisconnect();
|
||||
m_connection.m_tokens = {};
|
||||
|
||||
qDebug() << "Disconnection complete";
|
||||
}
|
||||
|
||||
bool ClientConnection::receiveAudioDto() const
|
||||
{
|
||||
return m_receiveAudioDto;
|
||||
}
|
||||
|
||||
void ClientConnection::setReceiveAudioDto(bool receiveAudioDto)
|
||||
{
|
||||
m_receiveAudioDto = receiveAudioDto;
|
||||
}
|
||||
|
||||
void ClientConnection::updateTransceivers(const QString &callsign, const QVector<TransceiverDto> &transceivers)
|
||||
{
|
||||
m_apiServerConnection.updateTransceivers(callsign, transceivers);
|
||||
}
|
||||
|
||||
void ClientConnection::connectToVoiceServer()
|
||||
{
|
||||
QHostAddress localAddress(QHostAddress::AnyIPv4);
|
||||
m_udpSocket.bind(localAddress);
|
||||
m_voiceServerTimer.start(3000);
|
||||
|
||||
qDebug() << "Connected to voice server (" + m_connection.m_tokens.VoiceServer.addressIpV4 << ")";
|
||||
}
|
||||
|
||||
void ClientConnection::disconnectFromVoiceServer()
|
||||
{
|
||||
m_voiceServerTimer.stop();
|
||||
m_udpSocket.disconnectFromHost();
|
||||
qDebug() << "All TaskVoiceServer tasks stopped";
|
||||
}
|
||||
|
||||
void ClientConnection::readPendingDatagrams()
|
||||
{
|
||||
while (m_udpSocket.hasPendingDatagrams())
|
||||
{
|
||||
QNetworkDatagram datagram = m_udpSocket.receiveDatagram();
|
||||
processMessage(datagram.data());
|
||||
}
|
||||
}
|
||||
|
||||
void ClientConnection::processMessage(const QByteArray &messageDdata, bool loopback)
|
||||
{
|
||||
CryptoDtoSerializer::Deserializer deserializer = CryptoDtoSerializer::deserialize(*m_connection.voiceCryptoChannel, messageDdata, loopback);
|
||||
|
||||
if(deserializer.dtoNameBuffer == AudioRxOnTransceiversDto::getShortDtoName())
|
||||
{
|
||||
// qDebug() << "Received audio data";
|
||||
AudioRxOnTransceiversDto audioOnTransceiverDto = deserializer.getDto<AudioRxOnTransceiversDto>();
|
||||
if (m_connection.m_receiveAudio && m_connection.m_connected)
|
||||
{
|
||||
emit audioReceived(audioOnTransceiverDto);
|
||||
}
|
||||
}
|
||||
else if(deserializer.dtoNameBuffer == HeartbeatAckDto::getShortDtoName())
|
||||
{
|
||||
m_connection.m_lastVoiceServerHeartbeatAckUtc = QDateTime::currentDateTimeUtc();
|
||||
qDebug() << "Received voice server heartbeat";
|
||||
}
|
||||
else
|
||||
{
|
||||
qWarning() << "Received unknown data:" << deserializer.dtoNameBuffer << deserializer.dataLength;
|
||||
}
|
||||
}
|
||||
|
||||
void ClientConnection::handleSocketError(QAbstractSocket::SocketError error)
|
||||
{
|
||||
Q_UNUSED(error);
|
||||
qDebug() << "UDP socket error" << m_udpSocket.errorString();
|
||||
}
|
||||
|
||||
void ClientConnection::voiceServerHeartbeat()
|
||||
{
|
||||
QUrl voiceServerUrl("udp://" + m_connection.m_tokens.VoiceServer.addressIpV4);
|
||||
qDebug() << "Sending voice server heartbeat to" << voiceServerUrl.host();
|
||||
HeartbeatDto keepAlive;
|
||||
keepAlive.callsign = m_connection.m_callsign.toStdString();
|
||||
QByteArray dataBytes = CryptoDtoSerializer::Serialize(*m_connection.voiceCryptoChannel, CryptoDtoMode::AEAD_ChaCha20Poly1305, keepAlive);
|
||||
m_udpSocket.writeDatagram(dataBytes, QHostAddress(voiceServerUrl.host()), voiceServerUrl.port());
|
||||
}
|
||||
79
src/blackcore/afv/connection/clientconnection.h
Normal file
79
src/blackcore/afv/connection/clientconnection.h
Normal file
@@ -0,0 +1,79 @@
|
||||
#ifndef CLIENTCONNECTION_H
|
||||
#define CLIENTCONNECTION_H
|
||||
|
||||
#include "blackcore/afv/crypto/cryptodtoserializer.h"
|
||||
#include "blackcore/afv/connection/clientconnectiondata.h"
|
||||
#include "blackcore/afv/connection/apiserverconnection.h"
|
||||
#include "blackcore/afv/dto.h"
|
||||
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
#include <QTimer>
|
||||
#include <QUdpSocket>
|
||||
|
||||
class ClientConnection : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
//! Com status
|
||||
enum ConnectionStatus
|
||||
{
|
||||
Disconnected, //!< Not connected
|
||||
Connected, //!< Connection established
|
||||
};
|
||||
Q_ENUM(ConnectionStatus)
|
||||
|
||||
ClientConnection(const QString &apiServer, QObject *parent = nullptr);
|
||||
|
||||
void connectTo(const QString &userName, const QString &password, const QString &callsign);
|
||||
void disconnectFrom(const QString &reason = {});
|
||||
|
||||
bool isConnected() const { return m_connection.m_connected; }
|
||||
|
||||
void setReceiveAudio(bool value) { m_connection.m_receiveAudio = value; }
|
||||
bool receiveAudio() const { return m_connection.m_receiveAudio; }
|
||||
|
||||
template<typename T>
|
||||
void sendToVoiceServer(T dto)
|
||||
{
|
||||
QUrl voiceServerUrl("udp://" + m_connection.m_tokens.VoiceServer.addressIpV4);
|
||||
QByteArray dataBytes = CryptoDtoSerializer::Serialize(*m_connection.voiceCryptoChannel, CryptoDtoMode::AEAD_ChaCha20Poly1305, dto);
|
||||
m_udpSocket.writeDatagram(dataBytes, QHostAddress(voiceServerUrl.host()), voiceServerUrl.port());
|
||||
}
|
||||
|
||||
bool receiveAudioDto() const;
|
||||
void setReceiveAudioDto(bool receiveAudioDto);
|
||||
|
||||
void updateTransceivers(const QString &callsign, const QVector<TransceiverDto> &transceivers);
|
||||
|
||||
signals:
|
||||
void audioReceived(const AudioRxOnTransceiversDto &dto);
|
||||
|
||||
private:
|
||||
void connectToVoiceServer();
|
||||
void disconnectFromVoiceServer();
|
||||
|
||||
void readPendingDatagrams();
|
||||
void processMessage(const QByteArray &messageDdata, bool loopback = false);
|
||||
void handleSocketError(QAbstractSocket::SocketError error);
|
||||
|
||||
void voiceServerHeartbeat();
|
||||
|
||||
const QUuid m_networkVersion = QUuid("3a5ddc6d-cf5d-4319-bd0e-d184f772db80");
|
||||
|
||||
//Data
|
||||
ClientConnectionData m_connection;
|
||||
|
||||
// Voice server
|
||||
QUdpSocket m_udpSocket;
|
||||
QTimer m_voiceServerTimer;
|
||||
|
||||
// API server
|
||||
ApiServerConnection m_apiServerConnection;
|
||||
|
||||
// Properties
|
||||
bool m_receiveAudioDto = true;
|
||||
};
|
||||
|
||||
#endif
|
||||
28
src/blackcore/afv/connection/clientconnectiondata.cpp
Normal file
28
src/blackcore/afv/connection/clientconnectiondata.cpp
Normal file
@@ -0,0 +1,28 @@
|
||||
#include "clientconnectiondata.h"
|
||||
#include <QDebug>
|
||||
|
||||
qint64 ClientConnectionData::secondsSinceAuthentication() const
|
||||
{
|
||||
return m_authenticatedDateTimeUtc.secsTo(QDateTime::currentDateTimeUtc());
|
||||
}
|
||||
|
||||
bool ClientConnectionData::isVoiceServerAlive() const
|
||||
{
|
||||
return m_lastVoiceServerHeartbeatAckUtc.secsTo(QDateTime::currentDateTimeUtc()) > serverTimeout;
|
||||
}
|
||||
|
||||
void ClientConnectionData::createCryptoChannels()
|
||||
{
|
||||
if (! m_tokens.isValid)
|
||||
{
|
||||
qWarning() << "Tokens not set";
|
||||
}
|
||||
voiceCryptoChannel.reset(new CryptoDtoChannel(m_tokens.VoiceServer.channelConfig));
|
||||
// dataCryptoChannel.reset(new CryptoDtoChannel(m_tokens.DataServer.channelConfig));
|
||||
}
|
||||
|
||||
bool ClientConnectionData::voiceServerAlive() const
|
||||
{
|
||||
return timeSinceAuthentication() < serverTimeout ||
|
||||
m_lastVoiceServerHeartbeatAckUtc.secsTo(QDateTime::currentDateTimeUtc()) < serverTimeout;
|
||||
}
|
||||
50
src/blackcore/afv/connection/clientconnectiondata.h
Normal file
50
src/blackcore/afv/connection/clientconnectiondata.h
Normal file
@@ -0,0 +1,50 @@
|
||||
#ifndef CLIENTCONNECTIONDATA_H
|
||||
#define CLIENTCONNECTIONDATA_H
|
||||
|
||||
#include "blackcore/afv/dto.h"
|
||||
#include "apiserverconnection.h"
|
||||
#include "blackcore/afv/crypto/cryptodtochannel.h"
|
||||
|
||||
#include <QDateTime>
|
||||
#include <QtGlobal>
|
||||
#include <QString>
|
||||
#include <QScopedPointer>
|
||||
|
||||
struct ClientConnectionData
|
||||
{
|
||||
ClientConnectionData() = default;
|
||||
|
||||
qint64 secondsSinceAuthentication() const;
|
||||
|
||||
bool isVoiceServerAlive() const;
|
||||
bool isDataServerAlive() const;
|
||||
|
||||
/* TODO
|
||||
public long VoiceServerBytesSent { get; set; }
|
||||
public long VoiceServerBytesReceived { get; set; }
|
||||
public long DataServerBytesSent { get; set; }
|
||||
public long DataServerBytesReceived { get; set; }
|
||||
*/
|
||||
|
||||
void createCryptoChannels();
|
||||
|
||||
qint64 timeSinceAuthentication() const { return m_authenticatedDateTimeUtc.secsTo(QDateTime::currentDateTimeUtc()); }
|
||||
bool voiceServerAlive() const;
|
||||
|
||||
QString m_userName;
|
||||
QString m_callsign;
|
||||
|
||||
PostCallsignResponseDto m_tokens;
|
||||
|
||||
QScopedPointer<CryptoDtoChannel> voiceCryptoChannel;
|
||||
|
||||
QDateTime m_authenticatedDateTimeUtc;
|
||||
QDateTime m_lastVoiceServerHeartbeatAckUtc;
|
||||
|
||||
bool m_receiveAudio = true;
|
||||
bool m_connected = false;
|
||||
|
||||
static constexpr qint64 serverTimeout = 10;
|
||||
};
|
||||
|
||||
#endif // CLIENTCONNECTIONDATA_H
|
||||
Reference in New Issue
Block a user