File: OdrsReviewsJob.cpp

package info (click to toggle)
plasma-discover 6.5.4-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 14,288 kB
  • sloc: cpp: 30,576; xml: 2,710; python: 311; sh: 5; makefile: 5
file content (160 lines) | stat: -rw-r--r-- 7,338 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
/*
 *   SPDX-FileCopyrightText: 2013 Aleix Pol Gonzalez <aleixpol@blue-systems.com>
 *   SPDX-FileCopyrightText: 2017 Jan Grulich <jgrulich@redhat.com>
 *
 *   SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
 */

#include "OdrsReviewsJob.h"
#include <ReviewsBackend/Review.h>
#include <ReviewsBackend/ReviewsModel.h>
#include <resources/AbstractResource.h>

#include <KLocalizedString>
#include <QJsonDocument>

#include "libdiscover_debug.h"

OdrsSubmitReviewsJob::OdrsSubmitReviewsJob(QNetworkReply *reply, AbstractResource *resource)
    : OdrsReviewsJob(reply, resource)
{
    connect(reply, &QNetworkReply::finished, this, &OdrsSubmitReviewsJob::reviewSubmitted);
}

void OdrsSubmitReviewsJob::reviewSubmitted()
{
    const auto networkError = m_reply->error();
    if (networkError == QNetworkReply::NoError) {
        qCWarning(LIBDISCOVER_LOG) << "OdrsReviewsBackend: Review submitted for" << m_resource;
        if (m_resource) {
            const QJsonDocument document({m_resource->getMetadata(QLatin1StringView("ODRS::review_map")).toObject()});
            parseReviews(document);
        } else {
            qCWarning(LIBDISCOVER_LOG) << "OdrsReviewsBackend: Failed to submit review: missing object";
        }
    } else {
        qCWarning(LIBDISCOVER_LOG).noquote() << "OdrsReviewsBackend: Failed to submit review:" << m_reply->error() << m_reply->errorString()
                                             << m_reply->rawHeaderPairs();
        Q_EMIT errorMessage(i18n("Error while submitting review: %1", m_reply->errorString()));
    }
}

OdrsReviewsJob::OdrsReviewsJob(QNetworkReply *reply, AbstractResource *resource)
    : m_reply(reply)
    , m_resource(resource)
{
    Q_ASSERT(m_reply);
    Q_ASSERT(m_resource);
}

OdrsReviewsJob *OdrsReviewsJob::create(QNetworkReply *reply, AbstractResource *resource)
{
    auto r = new OdrsReviewsJob(reply, resource);
    connect(reply, &QNetworkReply::finished, r, &OdrsReviewsJob::reviewsFetched);
    return r;
}

OdrsReviewsJob::~OdrsReviewsJob()
{
    delete m_reply;
}

void OdrsReviewsJob::reviewsFetched()
{
    const QByteArray data = m_reply->readAll();
    const auto networkError = m_reply->error();
    if (networkError != QNetworkReply::NoError) {
        qCWarning(LIBDISCOVER_LOG) << "OdrsReviewsBackend: Error fetching reviews:" << m_reply->errorString() << data;
        Q_EMIT errorMessage(i18n("Technical error message: %1", m_reply->errorString()));
        Q_EMIT reviewsReady({}, false);
        deleteLater();
        return;
    }

    QJsonParseError error;
    const QJsonDocument document = QJsonDocument::fromJson(data, &error);
    if (error.error) {
        qCWarning(LIBDISCOVER_LOG) << "OdrsReviewsBackend: Error parsing reviews:" << m_reply->url() << error.errorString();
    }
    parseReviews(document);
    deleteLater();
}

void OdrsReviewsJob::parseReviews(const QJsonDocument &document)
{
    const auto reviews = document.array();
    QList<ReviewPtr> reviewsList;
    for (const auto &it : reviews) {
        const QJsonObject review = it.toObject();
        if (!review.isEmpty()) {
            // Same ranking algorythm Gnome Software uses
            const int usefulFavorable = review.value(QLatin1StringView("karma_up")).toInt();
            const int usefulNegative = review.value(QLatin1StringView("karma_down")).toInt();
            const int usefulTotal = usefulFavorable + usefulNegative;

            qreal usefulWilson = 0.0;

            /* from http://www.evanmiller.org/how-not-to-sort-by-average-rating.html */
            if (usefulFavorable > 0 || usefulNegative > 0) {
                usefulWilson =
                    ((usefulFavorable + 1.9208) / (usefulFavorable + usefulNegative)
                     - 1.96 * sqrt((usefulFavorable * usefulNegative) / qreal(usefulFavorable + usefulNegative) + 0.9604) / (usefulFavorable + usefulNegative))
                    / (1 + 3.8416 / (usefulFavorable + usefulNegative));
                usefulWilson *= 100.0;
            }

            QDateTime dateTime;
            dateTime.setSecsSinceEpoch(review.value(QLatin1StringView("date_created")).toInt());

            // If there is no score or the score is the same, base on the age
            const auto currentDateTime = QDateTime::currentDateTime();
            const auto totalDays = static_cast<qreal>(dateTime.daysTo(currentDateTime));

            // use also the longest common subsequence of the version string to compute relevance
            const auto reviewVersion = review.value(QLatin1StringView("version")).toString();
            const auto availableVersion = m_resource->availableVersion();
            qreal versionScore = 0;
            const int minLength = std::min(reviewVersion.length(), availableVersion.length());
            if (minLength > 0) {
                for (int i = 0; i < minLength; ++i) {
                    if (reviewVersion[i] != availableVersion[i] || i == minLength - 1) {
                        versionScore = i;
                        break;
                    }
                }
                // Normalize
                versionScore = versionScore / qreal(std::max(reviewVersion.length(), availableVersion.length()) - 1);
            }

            // Very random heuristic which weights usefulness with age and version similarity. Don't penalize usefulness more than 6 months
            usefulWilson = versionScore + 1.0 / std::max(1.0, totalDays) + usefulWilson / std::clamp(totalDays, 1.0, 93.0);

            const bool shouldShow = usefulFavorable >= usefulNegative * 2 && review.value(QLatin1StringView("reported")).toInt() < 4;
            ReviewPtr r(new Review(review.value(QLatin1StringView("app_id")).toString(),
                                   m_resource->packageName(),
                                   review.value(QLatin1StringView("locale")).toString(),
                                   review.value(QLatin1StringView("summary")).toString(),
                                   review.value(QLatin1StringView("description")).toString(),
                                   review.value(QLatin1StringView("user_display")).toString(),
                                   dateTime,
                                   shouldShow,
                                   review.value(QLatin1StringView("review_id")).toInt(),
                                   review.value(QLatin1StringView("rating")).toInt() / 10,
                                   usefulTotal,
                                   usefulFavorable,
                                   usefulWilson,
                                   reviewVersion));
            // We can also receive just a json with app name and user info so filter these out as there is no review
            if (!r->summary().isEmpty() && !r->reviewText().isEmpty()) {
                reviewsList.append(r);
                // Needed for submitting usefulness
                r->addMetadata(QLatin1StringView("ODRS::user_skey"), review.value(QLatin1StringView("user_skey")).toString());
            }

            // We should get at least user_skey needed for posting reviews
            m_resource->addMetadata(QLatin1StringView("ODRS::user_skey"), review.value(QLatin1StringView("user_skey")).toString());
        }
    }

    Q_EMIT reviewsReady(reviewsList, false);
}