⛏️ index : haiku.git

/*
 * Copyright 2016-2025, Andrew Lindesay <apl@lindesay.co.nz>.
 * Copyright 2013-2014, Stephan Aßmus <superstippi@gmx.de>.
 * All rights reserved. Distributed under the terms of the MIT License.
 *
 * Note that this file included code earlier from `Model.cpp` and
 * copyrights have been latterly been carried across in 2024.
 */


#include "PopulatePkgUserRatingsFromServerProcess.h"

#include <Autolock.h>
#include <Catalog.h>

#include "Logger.h"
#include "PackageUtils.h"
#include "ServerHelper.h"


#undef B_TRANSLATION_CONTEXT
#define B_TRANSLATION_CONTEXT "PopulatePkgUserRatingsFromServerProcess"


PopulatePkgUserRatingsFromServerProcess::PopulatePkgUserRatingsFromServerProcess(
	const BString& packageName, Model* model)
	:
	fModel(model),
	fPackageName(packageName)
{
}


PopulatePkgUserRatingsFromServerProcess::~PopulatePkgUserRatingsFromServerProcess()
{
}


const char*
PopulatePkgUserRatingsFromServerProcess::Name() const
{
	return "PopulatePkgUserRatingsFromServerProcess";
}


const char*
PopulatePkgUserRatingsFromServerProcess::Description() const
{
	return B_TRANSLATE("Fetching user ratings for package");
}


status_t
PopulatePkgUserRatingsFromServerProcess::RunInternal()
{
	if (!ServerHelper::IsNetworkAvailable()) {
		HDINFO("no network so will not populate user ratings");
		return B_OK;
	}

	// TODO; use API spec to code generation techniques instead of this manually written client.

	status_t status = B_OK;

	// Retrieve info from web-app
	BMessage info;
	BString webAppRepositoryCode = _WebAppRepositoryCode();

	if (webAppRepositoryCode.IsEmpty()) {
		HDERROR("unable to get the web app repository code for pkg [%s]", fPackageName.String());
		status = B_ERROR;
	}

	if (status == B_OK) {
		status = fModel->WebApp()->RetrieveUserRatingsForPackageForDisplay(fPackageName,
			webAppRepositoryCode, BString(), 0, PACKAGE_INFO_MAX_USER_RATINGS, info);
			// ^ note intentionally not using the repository source code as this would then show
			// too few results as it would be architecture specific.
	}

	PackageUserRatingInfoBuilder userRatingInfoBuilder;

	if (status == B_OK) {
		// Parse message
		BMessage result;
		BMessage items;

		status = info.FindMessage("result", &result);

		if (status == B_OK) {

			if (result.FindMessage("items", &items) == B_OK) {

				int32 index = 0;
				while (true) {
					BString name;
					name << index++;

					BMessage item;
					if (items.FindMessage(name, &item) != B_OK)
						break;

					BString code;
					if (item.FindString("code", &code) != B_OK) {
						HDERROR("corrupt user rating at index %" B_PRIi32, index);
						continue;
					}

					BString user;
					BMessage userInfo;
					if (item.FindMessage("user", &userInfo) != B_OK
						|| userInfo.FindString("nickname", &user) != B_OK) {
						HDERROR("ignored user rating [%s] without a user nickname", code.String());
						continue;
					}

					// Extract basic info, all items are optional
					BString languageCode;
					BString comment;
					double rating;
					item.FindString("naturalLanguageCode", &languageCode);
					item.FindString("comment", &comment);
					if (item.FindDouble("rating", &rating) != B_OK)
						rating = -1;
					if (comment.Length() == 0 && rating == -1) {
						HDERROR("rating [%s] has no comment or rating so will"
								" be ignored",
							code.String());
						continue;
					}

					// For which version of the package was the rating?
					BString major = "?";
					BString minor = "?";
					BString micro = "";
					double revision = -1;
					BString architectureCode = "";
					BMessage version;
					if (item.FindMessage("pkgVersion", &version) == B_OK) {
						version.FindString("major", &major);
						version.FindString("minor", &minor);
						version.FindString("micro", &micro);
						version.FindDouble("revision", &revision);
						version.FindString("architectureCode", &architectureCode);
					}
					BString versionString = major;
					versionString << ".";
					versionString << minor;
					if (!micro.IsEmpty()) {
						versionString << ".";
						versionString << micro;
					}
					if (revision > 0) {
						versionString << "-";
						versionString << (int)revision;
					}

					if (!architectureCode.IsEmpty()) {
						versionString << " " << STR_MDASH << " ";
						versionString << architectureCode;
					}

					double createTimestamp;
					item.FindDouble("createTimestamp", &createTimestamp);

					// Add the rating to the PackageInfo
					UserRatingRef userRating(
						new UserRating(UserInfo(user), rating, comment, languageCode,
							// note that language identifiers are "code" in HDS and "id" in Haiku
							versionString, static_cast<uint64>(createTimestamp)),
						true);
					userRatingInfoBuilder.AddUserRating(userRating);
					HDDEBUG("rating [%s] retrieved from server", code.String());
				}

				userRatingInfoBuilder.WithUserRatingsPopulated();
				HDDEBUG("did retrieve %" B_PRIi32 " user ratings for [%s]", index - 1,
					fPackageName.String());
			}
		} else {
			int32 errorCode = WebAppInterface::ErrorCodeFromResponse(info);

			if (errorCode != ERROR_CODE_NONE)
				ServerHelper::NotifyServerJsonRpcError(info);
		}
	} else {
		ServerHelper::NotifyTransportError(status);
	}

	// Now fetch the user rating summary which is derived separately as it is
	// not just based on the user-ratings downloaded; it is calculated according
	// to an algorithm. This is best executed server-side to avoid discrepancy.

	BMessage summaryResponse;

	if (status == B_OK) {
		status = fModel->WebApp()->RetrieveUserRatingSummaryForPackage(fPackageName,
			webAppRepositoryCode, summaryResponse);
	}

	if (status == B_OK) {
		// Parse message
		UserRatingSummaryBuilder userRatingSummaryBuilder;

		BMessage result;

		// TODO; this entire BMessage handling is historical and needs to be swapped out with
		//	generated code from the API spec; it just takes time unfortunately.

		status = summaryResponse.FindMessage("result", &result);

		double sampleSizeF;
		int sampleSize = 0;
		bool hasData;

		if (status == B_OK)
			status = result.FindDouble("sampleSize", &sampleSizeF);

		if (status == B_OK) {
			sampleSize = static_cast<int>(sampleSizeF);
			userRatingSummaryBuilder.WithRatingCount(sampleSize);
		}

		hasData = status == B_OK && sampleSize > 0;

		if (hasData) {
			double ratingF;

			if (status == B_OK)
				status = result.FindDouble("rating", &ratingF);

			if (status == B_OK)
				userRatingSummaryBuilder.WithAverageRating(ratingF);
		}

		if (hasData) {
			BMessage ratingDistributionItems;
			BMessage item;

			status = result.FindMessage("ratingDistribution", &ratingDistributionItems);

			int32 index = 0;
			while (status == B_OK) {
				BString name;
				name << index++;

				BMessage ratingDistributionItem;
				if (ratingDistributionItems.FindMessage(name, &ratingDistributionItem) != B_OK)
					break;

				double ratingDistributionRatingF;

				if (status == B_OK) {
					status
						= ratingDistributionItem.FindDouble("rating", &ratingDistributionRatingF);
				}

				double ratingDistributionTotalF;

				if (status == B_OK)
					status = ratingDistributionItem.FindDouble("total", &ratingDistributionTotalF);

				userRatingSummaryBuilder.AddRatingByStar(
					static_cast<int>(ratingDistributionRatingF),
					static_cast<int>(ratingDistributionTotalF));
			}

			userRatingInfoBuilder.WithSummary(userRatingSummaryBuilder.BuildRef());
		} else {
			int32 errorCode = WebAppInterface::ErrorCodeFromResponse(summaryResponse);

			if (errorCode != ERROR_CODE_NONE)
				ServerHelper::NotifyServerJsonRpcError(summaryResponse);
		}
	} else {
		ServerHelper::NotifyTransportError(status);
	}

	if (status == B_OK) {
		PackageInfoRef package = fModel->PackageForName(fPackageName);

		PackageInfoRef updatedPackage = PackageInfoBuilder(package)
											.WithUserRatingInfo(userRatingInfoBuilder.BuildRef())
											.BuildRef();

		fModel->AddPackage(updatedPackage);
	}

	return status;
}


const BString
PopulatePkgUserRatingsFromServerProcess::_WebAppRepositoryCode() const
{
	const PackageInfoRef package = fModel->PackageForName(fPackageName);
	const BString depotName = PackageUtils::DepotName(package);
	const DepotInfoRef depot = fModel->DepotForName(depotName);

	if (depot.IsSet())
		return depot->WebAppRepositoryCode();

	return BString();
}