remote_suggestion.cc 10.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324
  1. // Copyright 2015 The Chromium Authors. All rights reserved.
  2. // Use of this source code is governed by a BSD-style license that can be
  3. // found in the LICENSE file.
  4. #include "components/ntp_snippets/remote/remote_suggestion.h"
  5. #include <limits>
  6. #include "base/logging.h"
  7. #include "base/memory/ptr_util.h"
  8. #include "base/strings/string_number_conversions.h"
  9. #include "base/strings/utf_string_conversions.h"
  10. #include "base/values.h"
  11. #include "components/ntp_snippets/category.h"
  12. #include "components/ntp_snippets/remote/proto/ntp_snippets.pb.h"
  13. #include "components/ntp_snippets/time_serialization.h"
  14. namespace {
  15. // dict.Get() specialization for base::Time values
  16. bool GetTimeValue(const base::Value::Dict& dict,
  17. const std::string& key,
  18. base::Time* time) {
  19. const std::string* time_value = dict.FindString(key);
  20. if (!time_value) {
  21. return false;
  22. }
  23. return base::Time::FromString(time_value->c_str(), time);
  24. }
  25. // dict.Get() specialization for GURL values
  26. bool GetURLValue(const base::Value::Dict& dict,
  27. const std::string& key,
  28. GURL* url) {
  29. const std::string* spec = dict.FindString(key);
  30. if (!spec) {
  31. return false;
  32. }
  33. *url = GURL(*spec);
  34. return url->is_valid();
  35. }
  36. // dict.Get() specialization for std::string values
  37. bool GetStringValue(const base::Value::Dict& dict,
  38. const std::string& key,
  39. std::string* str) {
  40. const std::string* str_value = dict.FindString(key);
  41. if (!str_value) {
  42. return false;
  43. }
  44. *str = *str_value;
  45. return true;
  46. }
  47. } // namespace
  48. namespace ntp_snippets {
  49. const int kArticlesRemoteId = 1;
  50. static_assert(
  51. static_cast<int>(KnownCategories::ARTICLES) -
  52. static_cast<int>(KnownCategories::REMOTE_CATEGORIES_OFFSET) ==
  53. kArticlesRemoteId,
  54. "kArticlesRemoteId has a wrong value?!");
  55. RemoteSuggestion::RemoteSuggestion(const std::vector<std::string>& ids,
  56. int remote_category_id)
  57. : ids_(ids),
  58. score_(0),
  59. is_dismissed_(false),
  60. remote_category_id_(remote_category_id),
  61. rank_(std::numeric_limits<int>::max()),
  62. should_notify_(false),
  63. content_type_(ContentType::UNKNOWN) {}
  64. RemoteSuggestion::~RemoteSuggestion() = default;
  65. // static
  66. std::unique_ptr<RemoteSuggestion>
  67. RemoteSuggestion::CreateFromContentSuggestionsDictionary(
  68. const base::Value::Dict& dict,
  69. int remote_category_id,
  70. const base::Time& fetch_date) {
  71. const base::Value::List* ids = dict.FindList("ids");
  72. if (!ids) {
  73. return nullptr;
  74. }
  75. std::vector<std::string> parsed_ids;
  76. for (const base::Value& value : *ids) {
  77. if (!value.is_string()) {
  78. return nullptr;
  79. }
  80. parsed_ids.push_back(value.GetString());
  81. }
  82. if (parsed_ids.empty()) {
  83. return nullptr;
  84. }
  85. auto snippet = MakeUnique(parsed_ids, remote_category_id);
  86. snippet->fetch_date_ = fetch_date;
  87. if (!(GetStringValue(dict, "title", &snippet->title_) &&
  88. GetTimeValue(dict, "creationTime", &snippet->publish_date_) &&
  89. GetTimeValue(dict, "expirationTime", &snippet->expiry_date_) &&
  90. GetStringValue(dict, "attribution", &snippet->publisher_name_) &&
  91. GetURLValue(dict, "fullPageUrl", &snippet->url_))) {
  92. return nullptr;
  93. }
  94. // Optional fields.
  95. GetStringValue(dict, "snippet", &snippet->snippet_);
  96. GetURLValue(dict, "imageUrl", &snippet->salient_image_url_);
  97. GetURLValue(dict, "ampUrl", &snippet->amp_url_);
  98. // TODO(sfiera): also favicon URL.
  99. const base::Value* image_dominant_color_value =
  100. dict.Find("imageDominantColor");
  101. if (image_dominant_color_value) {
  102. // The field is defined as fixed32 in the proto (effectively 32 bits
  103. // unsigned int), however, JSON does not support unsigned types. As a result
  104. // the value is parsed as int if it fits and as double otherwise. Double can
  105. // hold 32 bits precisely.
  106. uint32_t image_dominant_color;
  107. if (image_dominant_color_value->is_int()) {
  108. image_dominant_color = image_dominant_color_value->GetInt();
  109. } else if (image_dominant_color_value->is_double()) {
  110. image_dominant_color =
  111. static_cast<uint32_t>(image_dominant_color_value->GetDouble());
  112. }
  113. snippet->image_dominant_color_ = image_dominant_color;
  114. }
  115. absl::optional<double> score = dict.FindDouble("score");
  116. if (score)
  117. snippet->score_ = *score;
  118. if (const base::Value::Dict* notification_info =
  119. dict.FindDict("notificationInfo")) {
  120. absl::optional<bool> should_notify =
  121. notification_info->FindBool("shouldNotify");
  122. if (should_notify) {
  123. snippet->should_notify_ = should_notify.value();
  124. if (snippet->should_notify_) {
  125. if (!GetTimeValue(*notification_info, "deadline",
  126. &snippet->notification_deadline_)) {
  127. snippet->notification_deadline_ = base::Time::Max();
  128. }
  129. }
  130. }
  131. }
  132. // In the JSON dictionary contentType is an optional field. The field
  133. // content_type_ of the class |RemoteSuggestion| is by default initialized to
  134. // ContentType::UNKNOWN.
  135. std::string content_type;
  136. if (GetStringValue(dict, "contentType", &content_type)) {
  137. if (content_type == "VIDEO") {
  138. snippet->content_type_ = ContentType::VIDEO;
  139. } else {
  140. // The supported values are: VIDEO, UNKNOWN. Therefore if the field is
  141. // present the value has to be "UNKNOWN" here.
  142. DCHECK_EQ(content_type, "UNKNOWN");
  143. snippet->content_type_ = ContentType::UNKNOWN;
  144. }
  145. }
  146. return snippet;
  147. }
  148. // static
  149. std::unique_ptr<RemoteSuggestion> RemoteSuggestion::CreateFromProto(
  150. const SnippetProto& proto) {
  151. // Need at least the id.
  152. if (proto.ids_size() == 0 || proto.ids(0).empty()) {
  153. return nullptr;
  154. }
  155. int remote_category_id = proto.has_remote_category_id()
  156. ? proto.remote_category_id()
  157. : kArticlesRemoteId;
  158. std::vector<std::string> ids(proto.ids().begin(), proto.ids().end());
  159. auto snippet = MakeUnique(ids, remote_category_id);
  160. snippet->title_ = proto.title();
  161. snippet->snippet_ = proto.snippet();
  162. snippet->salient_image_url_ = GURL(proto.salient_image_url());
  163. if (proto.has_image_dominant_color()) {
  164. snippet->image_dominant_color_ = proto.image_dominant_color();
  165. }
  166. snippet->publish_date_ = DeserializeTime(proto.publish_date());
  167. snippet->expiry_date_ = DeserializeTime(proto.expiry_date());
  168. snippet->score_ = proto.score();
  169. snippet->is_dismissed_ = proto.dismissed();
  170. if (!proto.has_source()) {
  171. DLOG(WARNING) << "No source found for article " << snippet->id();
  172. return nullptr;
  173. }
  174. GURL url(proto.source().url());
  175. if (!url.is_valid()) {
  176. // We must at least have a valid source URL.
  177. DLOG(WARNING) << "Invalid article url " << proto.source().url();
  178. return nullptr;
  179. }
  180. GURL amp_url;
  181. if (proto.source().has_amp_url()) {
  182. amp_url = GURL(proto.source().amp_url());
  183. DLOG_IF(WARNING, !amp_url.is_valid())
  184. << "Invalid AMP URL " << proto.source().amp_url();
  185. }
  186. snippet->url_ = url;
  187. snippet->publisher_name_ = proto.source().publisher_name();
  188. snippet->amp_url_ = amp_url;
  189. if (proto.has_fetch_date()) {
  190. snippet->fetch_date_ = DeserializeTime(proto.fetch_date());
  191. }
  192. if (proto.content_type() == SnippetProto_ContentType_VIDEO) {
  193. snippet->content_type_ = ContentType::VIDEO;
  194. }
  195. snippet->rank_ =
  196. proto.has_rank() ? proto.rank() : std::numeric_limits<int>::max();
  197. return snippet;
  198. }
  199. SnippetProto RemoteSuggestion::ToProto() const {
  200. SnippetProto result;
  201. for (const std::string& id : ids_) {
  202. result.add_ids(id);
  203. }
  204. if (!title_.empty()) {
  205. result.set_title(title_);
  206. }
  207. if (!snippet_.empty()) {
  208. result.set_snippet(snippet_);
  209. }
  210. if (salient_image_url_.is_valid()) {
  211. result.set_salient_image_url(salient_image_url_.spec());
  212. }
  213. if (image_dominant_color_.has_value()) {
  214. result.set_image_dominant_color(*image_dominant_color_);
  215. }
  216. if (!publish_date_.is_null()) {
  217. result.set_publish_date(SerializeTime(publish_date_));
  218. }
  219. if (!expiry_date_.is_null()) {
  220. result.set_expiry_date(SerializeTime(expiry_date_));
  221. }
  222. result.set_score(score_);
  223. result.set_dismissed(is_dismissed_);
  224. result.set_remote_category_id(remote_category_id_);
  225. SnippetSourceProto* source_proto = result.mutable_source();
  226. source_proto->set_url(url_.spec());
  227. if (!publisher_name_.empty()) {
  228. source_proto->set_publisher_name(publisher_name_);
  229. }
  230. if (amp_url_.is_valid()) {
  231. source_proto->set_amp_url(amp_url_.spec());
  232. }
  233. if (!fetch_date_.is_null()) {
  234. result.set_fetch_date(SerializeTime(fetch_date_));
  235. }
  236. if (content_type_ == ContentType::VIDEO) {
  237. result.set_content_type(SnippetProto_ContentType_VIDEO);
  238. }
  239. result.set_rank(rank_);
  240. return result;
  241. }
  242. ContentSuggestion RemoteSuggestion::ToContentSuggestion(
  243. Category category) const {
  244. GURL url = url_;
  245. bool use_amp = !amp_url_.is_empty();
  246. if (use_amp) {
  247. url = amp_url_;
  248. }
  249. ContentSuggestion suggestion(category, id(), url);
  250. // Set url for fetching favicons if it differs from the main url (domains of
  251. // AMP URLs sometimes failed to provide favicons).
  252. if (use_amp) {
  253. suggestion.set_url_with_favicon(url_);
  254. }
  255. suggestion.set_title(base::UTF8ToUTF16(title_));
  256. suggestion.set_snippet_text(base::UTF8ToUTF16(snippet_));
  257. suggestion.set_publish_date(publish_date_);
  258. suggestion.set_publisher_name(base::UTF8ToUTF16(publisher_name_));
  259. suggestion.set_score(score_);
  260. suggestion.set_salient_image_url(salient_image_url_);
  261. if (should_notify_) {
  262. NotificationExtra extra;
  263. extra.deadline = notification_deadline_;
  264. suggestion.set_notification_extra(
  265. std::make_unique<NotificationExtra>(extra));
  266. }
  267. suggestion.set_fetch_date(fetch_date_);
  268. if (content_type_ == ContentType::VIDEO) {
  269. suggestion.set_is_video_suggestion(true);
  270. }
  271. suggestion.set_optional_image_dominant_color(image_dominant_color_);
  272. return suggestion;
  273. }
  274. // static
  275. std::unique_ptr<RemoteSuggestion> RemoteSuggestion::MakeUnique(
  276. const std::vector<std::string>& ids,
  277. int remote_category_id) {
  278. return base::WrapUnique(new RemoteSuggestion(ids, remote_category_id));
  279. }
  280. } // namespace ntp_snippets