/* SPDX-FileCopyrightText: 2026 Project Tick
* SPDX-FileContributor: Project Tick
* SPDX-License-Identifier: GPL-3.0-or-later
*
* MeshMC - A Custom Launcher for Minecraft
* Copyright (C) 2026 Project Tick
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*/
#include "ganalytics.h"
#include "ganalytics_worker.h"
#include "sys.h"
#include
#include
#include
#include
#include
const QLatin1String
GAnalyticsWorker::dateTimeFormat("yyyy,MM,dd-hh:mm::ss:zzz");
GAnalyticsWorker::GAnalyticsWorker(GAnalytics* parent)
: QObject(parent), q(parent), m_logLevel(GAnalytics::Error)
{
m_appName = QCoreApplication::instance()->applicationName();
m_appVersion = QCoreApplication::instance()->applicationVersion();
m_request.setUrl(QUrl("https://www.google-analytics.com/collect"));
m_request.setHeader(QNetworkRequest::ContentTypeHeader,
"application/x-www-form-urlencoded");
m_request.setHeader(QNetworkRequest::UserAgentHeader, getUserAgent());
m_language = QLocale::system().name().toLower().replace("_", "-");
m_screenResolution = getScreenResolution();
m_timer.setInterval(m_timerInterval);
connect(&m_timer, &QTimer::timeout, this, &GAnalyticsWorker::postMessage);
}
void GAnalyticsWorker::enable(bool state)
{
// state change to the same is not valid.
if (m_isEnabled == state) {
return;
}
m_isEnabled = state;
if (m_isEnabled) {
// enable -> start doing things :)
m_timer.start();
} else {
// disable -> stop the timer
m_timer.stop();
}
}
void GAnalyticsWorker::logMessage(GAnalytics::LogLevel level,
const QString& message)
{
if (m_logLevel > level) {
return;
}
qDebug() << "[Analytics]" << message;
}
/**
* Build the POST query. Adds all parameter to the query
* which are used in every POST.
* @param type Type of POST message. The event which is to post.
* @return query Most used parameter in a query for a POST.
*/
QUrlQuery GAnalyticsWorker::buildStandardPostQuery(const QString& type)
{
QUrlQuery query;
query.addQueryItem("v", "1");
query.addQueryItem("tid", m_trackingID);
query.addQueryItem("cid", m_clientID);
if (!m_userID.isEmpty()) {
query.addQueryItem("uid", m_userID);
}
query.addQueryItem("t", type);
query.addQueryItem("ul", m_language);
query.addQueryItem("vp", m_viewportSize);
query.addQueryItem("sr", m_screenResolution);
if (m_anonymizeIPs) {
query.addQueryItem("aip", "1");
}
return query;
}
/**
* Get primary screen resolution.
* @return A QString like "800x600".
*/
QString GAnalyticsWorker::getScreenResolution()
{
QScreen* screen = QGuiApplication::primaryScreen();
QSize size = screen->size();
return QString("%1x%2").arg(size.width()).arg(size.height());
}
/**
* Try to gain information about the system where this application
* is running. It needs to get the name and version of the operating
* system, the language and screen resolution.
* All this information will be send in POST messages.
* @return agent A QString with all the information formatted for a POST
* message.
*/
QString GAnalyticsWorker::getUserAgent()
{
return QString("%1/%2").arg(m_appName).arg(m_appVersion);
}
/**
* The message queue contains a list of QueryBuffer object.
* QueryBuffer holds a QUrlQuery object and a QDateTime object.
* These both object are freed from the buffer object and
* inserted as QString objects in a QList.
* @return dataList The list with concartinated queue data.
*/
QList GAnalyticsWorker::persistMessageQueue()
{
QList dataList;
foreach (QueryBuffer buffer, m_messageQueue) {
dataList << buffer.postQuery.toString();
dataList << buffer.time.toString(dateTimeFormat);
}
return dataList;
}
/**
* Reads persistent messages from a file.
* Gets all message data as a QList.
* Two lines in the list build a QueryBuffer object.
*/
void GAnalyticsWorker::readMessagesFromFile(const QList& dataList)
{
QListIterator iter(dataList);
while (iter.hasNext()) {
QString queryString = iter.next();
QString dateString = iter.next();
QUrlQuery query;
query.setQuery(queryString);
QDateTime dateTime = QDateTime::fromString(dateString, dateTimeFormat);
QueryBuffer buffer;
buffer.postQuery = query;
buffer.time = dateTime;
m_messageQueue.enqueue(buffer);
}
}
/**
* Takes a QUrlQuery object and wrapp it together with
* a QTime object into a QueryBuffer struct. These struct
* will be stored in the message queue.
*/
void GAnalyticsWorker::enqueQueryWithCurrentTime(const QUrlQuery& query)
{
QueryBuffer buffer;
buffer.postQuery = query;
buffer.time = QDateTime::currentDateTime();
m_messageQueue.enqueue(buffer);
}
/**
* This function is called by a timer interval.
* The function tries to send a messages from the queue.
* If message was successfully send then this function
* will be called back to send next message.
* If message queue contains more than one message then
* the connection will kept open.
* The message POST is asyncroniously when the server
* answered a signal will be emitted.
*/
void GAnalyticsWorker::postMessage()
{
if (m_messageQueue.isEmpty()) {
// queue empty -> try sending later
m_timer.start();
return;
} else {
// queue has messages -> stop timer and start sending
m_timer.stop();
}
QString connection = "close";
if (m_messageQueue.count() > 1) {
connection = "keep-alive";
}
QueryBuffer buffer = m_messageQueue.head();
QDateTime sendTime = QDateTime::currentDateTime();
qint64 timeDiff = buffer.time.msecsTo(sendTime);
if (timeDiff > fourHours) {
// too old.
m_messageQueue.dequeue();
emit postMessage();
return;
}
buffer.postQuery.addQueryItem("qt", QString::number(timeDiff));
m_request.setRawHeader("Connection", connection.toUtf8());
m_request.setHeader(QNetworkRequest::ContentLengthHeader,
buffer.postQuery.toString().length());
logMessage(GAnalytics::Debug,
"Query string = " + buffer.postQuery.toString());
// Create a new network access manager if we don't have one yet
if (networkManager == NULL) {
networkManager = new QNetworkAccessManager(this);
}
QNetworkReply* reply = networkManager->post(
m_request, buffer.postQuery.query(QUrl::EncodeUnicode).toUtf8());
connect(reply, SIGNAL(finished()), this, SLOT(postMessageFinished()));
}
/**
* NetworkAccsessManager has finished to POST a message.
* If POST message was successfully send then the message
* query should be removed from queue.
* SIGNAL "postMessage" will be emitted to send next message
* if there is any.
* If message couldn't be send then next try is when the
* timer emits its signal.
*/
void GAnalyticsWorker::postMessageFinished()
{
QNetworkReply* reply = qobject_cast(sender());
int httpStausCode =
reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
if (httpStausCode < 200 || httpStausCode > 299) {
logMessage(
GAnalytics::Error,
QString("Error posting message: %1").arg(reply->errorString()));
// An error ocurred. Try sending later.
m_timer.start();
return;
} else {
logMessage(GAnalytics::Debug, "Message sent");
}
m_messageQueue.dequeue();
postMessage();
reply->deleteLater();
}