1 /* 2 * Copyright 2016-2024, Andrew Lindesay <apl@lindesay.co.nz>. 3 * Copyright 2013-2014, Stephan Aßmus <superstippi@gmx.de>. 4 * All rights reserved. Distributed under the terms of the MIT License. 5 * 6 * Note that this file included code earlier from `Model.cpp` and 7 * copyrights have been latterly been carried across in 2024. 8 */ 9 10 11 #include "PopulatePkgUserRatingsFromServerProcess.h" 12 13 #include <Autolock.h> 14 #include <Catalog.h> 15 16 #include "Logger.h" 17 #include "ServerHelper.h" 18 19 20 #undef B_TRANSLATION_CONTEXT 21 #define B_TRANSLATION_CONTEXT "PopulatePkgUserRatingsFromServerProcess" 22 23 24 PopulatePkgUserRatingsFromServerProcess::PopulatePkgUserRatingsFromServerProcess( 25 PackageInfoRef packageInfo, Model* model) 26 : 27 fModel(model), 28 fPackageInfo(packageInfo) 29 { 30 } 31 32 33 PopulatePkgUserRatingsFromServerProcess::~PopulatePkgUserRatingsFromServerProcess() 34 { 35 } 36 37 38 const char* 39 PopulatePkgUserRatingsFromServerProcess::Name() const 40 { 41 return "PopulatePkgUserRatingsFromServerProcess"; 42 } 43 44 45 const char* 46 PopulatePkgUserRatingsFromServerProcess::Description() const 47 { 48 return B_TRANSLATE("Fetching user ratings for package"); 49 } 50 51 52 status_t 53 PopulatePkgUserRatingsFromServerProcess::RunInternal() 54 { 55 // TODO; use API spec to code generation techniques instead of this manually written client. 56 57 status_t status = B_OK; 58 59 // Retrieve info from web-app 60 BMessage info; 61 62 BString packageName; 63 BString webAppRepositoryCode; 64 65 { 66 BAutolock locker(&fLock); 67 packageName = fPackageInfo->Name(); 68 const DepotInfo* depot = fModel->DepotForName(fPackageInfo->DepotName()); 69 70 if (depot != NULL) 71 webAppRepositoryCode = depot->WebAppRepositoryCode(); 72 } 73 74 if (status == B_OK) { 75 status = fModel->GetWebAppInterface()->RetrieveUserRatingsForPackageForDisplay(packageName, 76 webAppRepositoryCode, BString(), 0, PACKAGE_INFO_MAX_USER_RATINGS, info); 77 // ^ note intentionally not using the repository source code as this would then show 78 // too few results as it would be architecture specific. 79 } 80 81 UserRatingInfoRef userRatingInfo(new UserRatingInfo(), true); 82 83 if (status == B_OK) { 84 // Parse message 85 BMessage result; 86 BMessage items; 87 88 status = info.FindMessage("result", &result); 89 90 if (status == B_OK) { 91 92 if (result.FindMessage("items", &items) == B_OK) { 93 94 int32 index = 0; 95 while (true) { 96 BString name; 97 name << index++; 98 99 BMessage item; 100 if (items.FindMessage(name, &item) != B_OK) 101 break; 102 103 BString code; 104 if (item.FindString("code", &code) != B_OK) { 105 HDERROR("corrupt user rating at index %" B_PRIi32, index); 106 continue; 107 } 108 109 BString user; 110 BMessage userInfo; 111 if (item.FindMessage("user", &userInfo) != B_OK 112 || userInfo.FindString("nickname", &user) != B_OK) { 113 HDERROR("ignored user rating [%s] without a user nickname", code.String()); 114 continue; 115 } 116 117 // Extract basic info, all items are optional 118 BString languageCode; 119 BString comment; 120 double rating; 121 item.FindString("naturalLanguageCode", &languageCode); 122 item.FindString("comment", &comment); 123 if (item.FindDouble("rating", &rating) != B_OK) 124 rating = -1; 125 if (comment.Length() == 0 && rating == -1) { 126 HDERROR("rating [%s] has no comment or rating so will" 127 " be ignored", 128 code.String()); 129 continue; 130 } 131 132 // For which version of the package was the rating? 133 BString major = "?"; 134 BString minor = "?"; 135 BString micro = ""; 136 double revision = -1; 137 BString architectureCode = ""; 138 BMessage version; 139 if (item.FindMessage("pkgVersion", &version) == B_OK) { 140 version.FindString("major", &major); 141 version.FindString("minor", &minor); 142 version.FindString("micro", µ); 143 version.FindDouble("revision", &revision); 144 version.FindString("architectureCode", &architectureCode); 145 } 146 BString versionString = major; 147 versionString << "."; 148 versionString << minor; 149 if (!micro.IsEmpty()) { 150 versionString << "."; 151 versionString << micro; 152 } 153 if (revision > 0) { 154 versionString << "-"; 155 versionString << (int)revision; 156 } 157 158 if (!architectureCode.IsEmpty()) { 159 versionString << " " << STR_MDASH << " "; 160 versionString << architectureCode; 161 } 162 163 double createTimestamp; 164 item.FindDouble("createTimestamp", &createTimestamp); 165 166 // Add the rating to the PackageInfo 167 UserRatingRef userRating( 168 new UserRating(UserInfo(user), rating, comment, languageCode, 169 // note that language identifiers are "code" in HDS and "id" in Haiku 170 versionString, (uint64) createTimestamp), 171 true); 172 userRatingInfo->AddUserRating(userRating); 173 HDDEBUG("rating [%s] retrieved from server", code.String()); 174 } 175 176 userRatingInfo->SetUserRatingsPopulated(); 177 HDDEBUG("did retrieve %" B_PRIi32 " user ratings for [%s]", index - 1, 178 packageName.String()); 179 } 180 } else { 181 int32 errorCode = WebAppInterface::ErrorCodeFromResponse(info); 182 183 if (errorCode != ERROR_CODE_NONE) 184 ServerHelper::NotifyServerJsonRpcError(info); 185 } 186 } else { 187 ServerHelper::NotifyTransportError(status); 188 } 189 190 // Now fetch the user rating summary which is derived separately as it is 191 // not just based on the user-ratings downloaded; it is calculated according 192 // to an algorithm. This is best executed server-side to avoid discrepancy. 193 194 BMessage summaryResponse; 195 196 if (status == B_OK) { 197 status = fModel->GetWebAppInterface()->RetrieveUserRatingSummaryForPackage(packageName, 198 webAppRepositoryCode, summaryResponse); 199 } 200 201 if (status == B_OK) { 202 // Parse message 203 204 UserRatingSummaryRef userRatingSummary(new UserRatingSummary(), true); 205 206 BMessage result; 207 208 // TODO; this entire BMessage handling is historical and needs to be swapped out with 209 // generated code from the API spec; it just takes time unfortunately. 210 211 status = summaryResponse.FindMessage("result", &result); 212 213 double sampleSizeF; 214 bool hasData; 215 216 if (status == B_OK) 217 status = result.FindDouble("sampleSize", &sampleSizeF); 218 219 if (status == B_OK) 220 userRatingSummary->SetRatingCount(static_cast<int>(sampleSizeF)); 221 222 hasData = status == B_OK && userRatingSummary->RatingCount() > 0; 223 224 if (hasData) { 225 double ratingF; 226 227 if (status == B_OK) 228 status = result.FindDouble("rating", &ratingF); 229 230 if (status == B_OK) 231 userRatingSummary->SetAverageRating(ratingF); 232 } 233 234 if (hasData) { 235 BMessage ratingDistributionItems; 236 BMessage item; 237 238 status = result.FindMessage("ratingDistribution", &ratingDistributionItems); 239 240 int32 index = 0; 241 while (status == B_OK) { 242 BString name; 243 name << index++; 244 245 BMessage ratingDistributionItem; 246 if (ratingDistributionItems.FindMessage(name, &ratingDistributionItem) != B_OK) 247 break; 248 249 double ratingDistributionRatingF; 250 251 if (status == B_OK) { 252 status 253 = ratingDistributionItem.FindDouble("rating", &ratingDistributionRatingF); 254 } 255 256 double ratingDistributionTotalF; 257 258 if (status == B_OK) 259 status = ratingDistributionItem.FindDouble("total", &ratingDistributionTotalF); 260 261 userRatingSummary->SetRatingByStar(static_cast<int>(ratingDistributionRatingF), 262 static_cast<int>(ratingDistributionTotalF)); 263 } 264 265 userRatingInfo->SetSummary(userRatingSummary); 266 } else { 267 int32 errorCode = WebAppInterface::ErrorCodeFromResponse(summaryResponse); 268 269 if (errorCode != ERROR_CODE_NONE) 270 ServerHelper::NotifyServerJsonRpcError(summaryResponse); 271 } 272 } else { 273 ServerHelper::NotifyTransportError(status); 274 } 275 276 if (status == B_OK) { 277 // TODO; later make the PackageInfo immutable to avoid the need for locking here. 278 BAutolock locker(&fLock); 279 fPackageInfo->SetUserRatingInfo(userRatingInfo); 280 } 281 282 return status; 283 } 284