1 //
2 // Copyright Aliaksei Levin (levlam@telegram.org), Arseny Smirnov (arseny30@gmail.com) 2014-2021
3 //
4 // Distributed under the Boost Software License, Version 1.0. (See accompanying
5 // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
6 //
7 #include "td/telegram/Photo.h"
8 
9 #include "td/telegram/files/FileEncryptionKey.h"
10 #include "td/telegram/files/FileLocation.h"
11 #include "td/telegram/files/FileManager.h"
12 #include "td/telegram/net/DcId.h"
13 
14 #include "td/utils/algorithm.h"
15 #include "td/utils/base64.h"
16 #include "td/utils/common.h"
17 #include "td/utils/format.h"
18 #include "td/utils/HttpUrl.h"
19 #include "td/utils/logging.h"
20 #include "td/utils/misc.h"
21 #include "td/utils/Random.h"
22 #include "td/utils/SliceBuilder.h"
23 
24 #include <algorithm>
25 #include <cmath>
26 #include <limits>
27 
28 namespace td {
29 
get_dimension(int32 size,const char * source)30 static uint16 get_dimension(int32 size, const char *source) {
31   if (size < 0 || size > 65535) {
32     LOG(ERROR) << "Wrong image dimension = " << size << " from " << source;
33     return 0;
34   }
35   return narrow_cast<uint16>(size);
36 }
37 
get_dimensions(int32 width,int32 height,const char * source)38 Dimensions get_dimensions(int32 width, int32 height, const char *source) {
39   Dimensions result;
40   result.width = get_dimension(width, source);
41   result.height = get_dimension(height, source);
42   if (result.width == 0 || result.height == 0) {
43     result.width = 0;
44     result.height = 0;
45   }
46   return result;
47 }
48 
get_pixel_count(const Dimensions & dimensions)49 static uint32 get_pixel_count(const Dimensions &dimensions) {
50   return static_cast<uint32>(dimensions.width) * static_cast<uint32>(dimensions.height);
51 }
52 
operator ==(const Dimensions & lhs,const Dimensions & rhs)53 bool operator==(const Dimensions &lhs, const Dimensions &rhs) {
54   return lhs.width == rhs.width && lhs.height == rhs.height;
55 }
56 
operator !=(const Dimensions & lhs,const Dimensions & rhs)57 bool operator!=(const Dimensions &lhs, const Dimensions &rhs) {
58   return !(lhs == rhs);
59 }
60 
operator <<(StringBuilder & string_builder,const Dimensions & dimensions)61 StringBuilder &operator<<(StringBuilder &string_builder, const Dimensions &dimensions) {
62   return string_builder << "(" << dimensions.width << ", " << dimensions.height << ")";
63 }
64 
get_minithumbnail_object(const string & packed)65 td_api::object_ptr<td_api::minithumbnail> get_minithumbnail_object(const string &packed) {
66   if (packed.size() < 3) {
67     return nullptr;
68   }
69   if (packed[0] == '\x01') {
70     static const string header =
71         base64_decode(
72             "/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDACgcHiMeGSgjISMtKygwPGRBPDc3PHtYXUlkkYCZlo+AjIqgtObDoKrarYqMyP/L2u71////"
73             "m8H///"
74             "/6/+b9//j/2wBDASstLTw1PHZBQXb4pYyl+Pj4+Pj4+Pj4+Pj4+Pj4+Pj4+Pj4+Pj4+Pj4+Pj4+Pj4+Pj4+Pj4+Pj4+Pj4+Pj4+Pj/"
75             "wAARCAAAAAADASIAAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/"
76             "8QAtRAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0R"
77             "FRkd"
78             "ISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2"
79             "uHi4"
80             "+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL/"
81             "8QAtREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVYnLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkN"
82             "ERUZ"
83             "HSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6goOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2"
84             "Nna4"
85             "uPk5ebn6Onq8vP09fb3+Pn6/9oADAMBAAIRAxEAPwA=")
86             .move_as_ok();
87     static const string footer = base64_decode("/9k=").move_as_ok();
88     auto result = td_api::make_object<td_api::minithumbnail>();
89     result->height_ = static_cast<unsigned char>(packed[1]);
90     result->width_ = static_cast<unsigned char>(packed[2]);
91     result->data_ = PSTRING() << header.substr(0, 164) << packed[1] << header[165] << packed[2] << header.substr(167)
92                               << packed.substr(3) << footer;
93     return result;
94   }
95   return nullptr;
96 }
97 
get_thumbnail_format_object(PhotoFormat format)98 static td_api::object_ptr<td_api::ThumbnailFormat> get_thumbnail_format_object(PhotoFormat format) {
99   switch (format) {
100     case PhotoFormat::Jpeg:
101       return td_api::make_object<td_api::thumbnailFormatJpeg>();
102     case PhotoFormat::Png:
103       return td_api::make_object<td_api::thumbnailFormatPng>();
104     case PhotoFormat::Webp:
105       return td_api::make_object<td_api::thumbnailFormatWebp>();
106     case PhotoFormat::Gif:
107       return td_api::make_object<td_api::thumbnailFormatGif>();
108     case PhotoFormat::Tgs:
109       return td_api::make_object<td_api::thumbnailFormatTgs>();
110     case PhotoFormat::Mpeg4:
111       return td_api::make_object<td_api::thumbnailFormatMpeg4>();
112     default:
113       UNREACHABLE();
114       return nullptr;
115   }
116 }
117 
operator <<(StringBuilder & string_builder,PhotoFormat format)118 static StringBuilder &operator<<(StringBuilder &string_builder, PhotoFormat format) {
119   switch (format) {
120     case PhotoFormat::Jpeg:
121       return string_builder << "jpg";
122     case PhotoFormat::Png:
123       return string_builder << "png";
124     case PhotoFormat::Webp:
125       return string_builder << "webp";
126     case PhotoFormat::Gif:
127       return string_builder << "gif";
128     case PhotoFormat::Tgs:
129       return string_builder << "tgs";
130     case PhotoFormat::Mpeg4:
131       return string_builder << "mp4";
132     default:
133       UNREACHABLE();
134       return string_builder;
135   }
136 }
137 
register_photo(FileManager * file_manager,const PhotoSizeSource & source,int64 id,int64 access_hash,std::string file_reference,DialogId owner_dialog_id,int32 file_size,DcId dc_id,PhotoFormat format)138 static FileId register_photo(FileManager *file_manager, const PhotoSizeSource &source, int64 id, int64 access_hash,
139                              std::string file_reference, DialogId owner_dialog_id, int32 file_size, DcId dc_id,
140                              PhotoFormat format) {
141   LOG(DEBUG) << "Receive " << format << " photo " << id << " of type " << source.get_file_type("register_photo")
142              << " from " << dc_id;
143   auto suggested_name = PSTRING() << source.get_unique_name(id) << '.' << format;
144   auto file_location_source = owner_dialog_id.get_type() == DialogType::SecretChat ? FileLocationSource::FromUser
145                                                                                    : FileLocationSource::FromServer;
146   return file_manager->register_remote(
147       FullRemoteFileLocation(source, id, access_hash, dc_id, std::move(file_reference)), file_location_source,
148       owner_dialog_id, file_size, 0, std::move(suggested_name));
149 }
150 
get_profile_photo(FileManager * file_manager,UserId user_id,int64 user_access_hash,tl_object_ptr<telegram_api::UserProfilePhoto> && profile_photo_ptr)151 ProfilePhoto get_profile_photo(FileManager *file_manager, UserId user_id, int64 user_access_hash,
152                                tl_object_ptr<telegram_api::UserProfilePhoto> &&profile_photo_ptr) {
153   ProfilePhoto result;
154   int32 profile_photo_id =
155       profile_photo_ptr == nullptr ? telegram_api::userProfilePhotoEmpty::ID : profile_photo_ptr->get_id();
156   switch (profile_photo_id) {
157     case telegram_api::userProfilePhotoEmpty::ID:
158       break;
159     case telegram_api::userProfilePhoto::ID: {
160       auto profile_photo = move_tl_object_as<telegram_api::userProfilePhoto>(profile_photo_ptr);
161 
162       auto dc_id = DcId::create(profile_photo->dc_id_);
163       result.has_animation = profile_photo->has_video_;
164       result.id = profile_photo->photo_id_;
165       result.minithumbnail = profile_photo->stripped_thumb_.as_slice().str();
166       result.small_file_id = register_photo(
167           file_manager, PhotoSizeSource::dialog_photo(DialogId(user_id), user_access_hash, false), result.id,
168           0 /*access_hash*/, "" /*file_reference*/, DialogId(), 0 /*file_size*/, dc_id, PhotoFormat::Jpeg);
169       result.big_file_id = register_photo(
170           file_manager, PhotoSizeSource::dialog_photo(DialogId(user_id), user_access_hash, true), result.id,
171           0 /*access_hash*/, "" /*file_reference*/, DialogId(), 0 /*file_size*/, dc_id, PhotoFormat::Jpeg);
172       break;
173     }
174     default:
175       UNREACHABLE();
176       break;
177   }
178 
179   return result;
180 }
181 
get_profile_photo_object(FileManager * file_manager,const ProfilePhoto & profile_photo)182 tl_object_ptr<td_api::profilePhoto> get_profile_photo_object(FileManager *file_manager,
183                                                              const ProfilePhoto &profile_photo) {
184   if (!profile_photo.small_file_id.is_valid()) {
185     return nullptr;
186   }
187   return td_api::make_object<td_api::profilePhoto>(
188       profile_photo.id, file_manager->get_file_object(profile_photo.small_file_id),
189       file_manager->get_file_object(profile_photo.big_file_id), get_minithumbnail_object(profile_photo.minithumbnail),
190       profile_photo.has_animation);
191 }
192 
operator ==(const ProfilePhoto & lhs,const ProfilePhoto & rhs)193 bool operator==(const ProfilePhoto &lhs, const ProfilePhoto &rhs) {
194   bool location_differs = lhs.small_file_id != rhs.small_file_id || lhs.big_file_id != rhs.big_file_id;
195   bool id_differs = lhs.id != rhs.id;
196 
197   if (location_differs) {
198     return false;
199   }
200   return lhs.has_animation == rhs.has_animation && lhs.minithumbnail == rhs.minithumbnail && !id_differs;
201 }
202 
operator !=(const ProfilePhoto & lhs,const ProfilePhoto & rhs)203 bool operator!=(const ProfilePhoto &lhs, const ProfilePhoto &rhs) {
204   return !(lhs == rhs);
205 }
206 
operator <<(StringBuilder & string_builder,const ProfilePhoto & profile_photo)207 StringBuilder &operator<<(StringBuilder &string_builder, const ProfilePhoto &profile_photo) {
208   return string_builder << "<ID = " << profile_photo.id << ", small_file_id = " << profile_photo.small_file_id
209                         << ", big_file_id = " << profile_photo.big_file_id
210                         << ", has_animation = " << profile_photo.has_animation << ">";
211 }
212 
get_dialog_photo(FileManager * file_manager,DialogId dialog_id,int64 dialog_access_hash,tl_object_ptr<telegram_api::ChatPhoto> && chat_photo_ptr)213 DialogPhoto get_dialog_photo(FileManager *file_manager, DialogId dialog_id, int64 dialog_access_hash,
214                              tl_object_ptr<telegram_api::ChatPhoto> &&chat_photo_ptr) {
215   int32 chat_photo_id = chat_photo_ptr == nullptr ? telegram_api::chatPhotoEmpty::ID : chat_photo_ptr->get_id();
216 
217   DialogPhoto result;
218   switch (chat_photo_id) {
219     case telegram_api::chatPhotoEmpty::ID:
220       break;
221     case telegram_api::chatPhoto::ID: {
222       auto chat_photo = move_tl_object_as<telegram_api::chatPhoto>(chat_photo_ptr);
223 
224       auto dc_id = DcId::create(chat_photo->dc_id_);
225       result.has_animation = chat_photo->has_video_;
226       result.minithumbnail = chat_photo->stripped_thumb_.as_slice().str();
227       result.small_file_id =
228           register_photo(file_manager, PhotoSizeSource::dialog_photo(dialog_id, dialog_access_hash, false),
229                          chat_photo->photo_id_, 0, "", DialogId(), 0, dc_id, PhotoFormat::Jpeg);
230       result.big_file_id =
231           register_photo(file_manager, PhotoSizeSource::dialog_photo(dialog_id, dialog_access_hash, true),
232                          chat_photo->photo_id_, 0, "", DialogId(), 0, dc_id, PhotoFormat::Jpeg);
233 
234       break;
235     }
236     default:
237       UNREACHABLE();
238       break;
239   }
240 
241   return result;
242 }
243 
get_chat_photo_info_object(FileManager * file_manager,const DialogPhoto * dialog_photo)244 tl_object_ptr<td_api::chatPhotoInfo> get_chat_photo_info_object(FileManager *file_manager,
245                                                                 const DialogPhoto *dialog_photo) {
246   if (dialog_photo == nullptr || !dialog_photo->small_file_id.is_valid()) {
247     return nullptr;
248   }
249   return td_api::make_object<td_api::chatPhotoInfo>(file_manager->get_file_object(dialog_photo->small_file_id),
250                                                     file_manager->get_file_object(dialog_photo->big_file_id),
251                                                     get_minithumbnail_object(dialog_photo->minithumbnail),
252                                                     dialog_photo->has_animation);
253 }
254 
dialog_photo_get_file_ids(const DialogPhoto & dialog_photo)255 vector<FileId> dialog_photo_get_file_ids(const DialogPhoto &dialog_photo) {
256   vector<FileId> result;
257   if (dialog_photo.small_file_id.is_valid()) {
258     result.push_back(dialog_photo.small_file_id);
259   }
260   if (dialog_photo.big_file_id.is_valid()) {
261     result.push_back(dialog_photo.big_file_id);
262   }
263   return result;
264 }
265 
as_fake_dialog_photo(const Photo & photo,DialogId dialog_id)266 DialogPhoto as_fake_dialog_photo(const Photo &photo, DialogId dialog_id) {
267   DialogPhoto result;
268   if (!photo.is_empty()) {
269     for (auto &size : photo.photos) {
270       if (size.type == 'a') {
271         result.small_file_id = size.file_id;
272       } else if (size.type == 'c') {
273         result.big_file_id = size.file_id;
274       }
275     }
276     result.minithumbnail = photo.minithumbnail;
277     result.has_animation = !photo.animations.empty();
278     if (!result.small_file_id.is_valid() || !result.big_file_id.is_valid()) {
279       LOG(ERROR) << "Failed to convert " << photo << " to chat photo of " << dialog_id;
280       return DialogPhoto();
281     }
282   }
283   return result;
284 }
285 
as_profile_photo(FileManager * file_manager,UserId user_id,int64 user_access_hash,const Photo & photo)286 ProfilePhoto as_profile_photo(FileManager *file_manager, UserId user_id, int64 user_access_hash, const Photo &photo) {
287   ProfilePhoto result;
288   static_cast<DialogPhoto &>(result) = as_fake_dialog_photo(photo, DialogId(user_id));
289   if (!result.small_file_id.is_valid()) {
290     return result;
291   }
292 
293   auto reregister_photo = [&](bool is_big, FileId file_id) {
294     auto file_view = file_manager->get_file_view(file_id);
295     CHECK(file_view.has_remote_location());
296     auto remote = file_view.remote_location();
297     CHECK(remote.is_photo());
298     CHECK(!remote.is_web());
299     remote.set_source(PhotoSizeSource::dialog_photo(DialogId(user_id), user_access_hash, is_big));
300     return file_manager->register_remote(std::move(remote), FileLocationSource::FromServer, DialogId(),
301                                          file_view.size(), file_view.expected_size(), file_view.remote_name());
302   };
303 
304   result.id = photo.id.get();
305   result.small_file_id = reregister_photo(false, result.small_file_id);
306   result.big_file_id = reregister_photo(true, result.big_file_id);
307 
308   return result;
309 }
310 
operator ==(const DialogPhoto & lhs,const DialogPhoto & rhs)311 bool operator==(const DialogPhoto &lhs, const DialogPhoto &rhs) {
312   return lhs.small_file_id == rhs.small_file_id && lhs.big_file_id == rhs.big_file_id &&
313          lhs.minithumbnail == rhs.minithumbnail && lhs.has_animation == rhs.has_animation;
314 }
315 
operator !=(const DialogPhoto & lhs,const DialogPhoto & rhs)316 bool operator!=(const DialogPhoto &lhs, const DialogPhoto &rhs) {
317   return !(lhs == rhs);
318 }
319 
operator <<(StringBuilder & string_builder,const DialogPhoto & dialog_photo)320 StringBuilder &operator<<(StringBuilder &string_builder, const DialogPhoto &dialog_photo) {
321   return string_builder << "<small_file_id = " << dialog_photo.small_file_id
322                         << ", big_file_id = " << dialog_photo.big_file_id
323                         << ", has_animation = " << dialog_photo.has_animation << ">";
324 }
325 
get_secret_thumbnail_photo_size(FileManager * file_manager,BufferSlice bytes,DialogId owner_dialog_id,int32 width,int32 height)326 PhotoSize get_secret_thumbnail_photo_size(FileManager *file_manager, BufferSlice bytes, DialogId owner_dialog_id,
327                                           int32 width, int32 height) {
328   if (bytes.empty()) {
329     return PhotoSize();
330   }
331   PhotoSize res;
332   res.type = 't';
333   res.dimensions = get_dimensions(width, height, "get_secret_thumbnail_photo_size");
334   res.size = narrow_cast<int32>(bytes.size());
335 
336   // generate some random remote location to save
337   auto dc_id = DcId::invalid();
338   auto photo_id = -(Random::secure_int64() & std::numeric_limits<int64>::max());
339 
340   res.file_id = file_manager->register_remote(
341       FullRemoteFileLocation(PhotoSizeSource::thumbnail(FileType::EncryptedThumbnail, 't'), photo_id, 0, dc_id,
342                              string()),
343       FileLocationSource::FromServer, owner_dialog_id, res.size, 0,
344       PSTRING() << static_cast<uint64>(photo_id) << ".jpg");
345   file_manager->set_content(res.file_id, std::move(bytes));
346 
347   return res;
348 }
349 
get_photo_size(FileManager * file_manager,PhotoSizeSource source,int64 id,int64 access_hash,std::string file_reference,DcId dc_id,DialogId owner_dialog_id,tl_object_ptr<telegram_api::PhotoSize> && size_ptr,PhotoFormat format)350 Variant<PhotoSize, string> get_photo_size(FileManager *file_manager, PhotoSizeSource source, int64 id,
351                                           int64 access_hash, std::string file_reference, DcId dc_id,
352                                           DialogId owner_dialog_id, tl_object_ptr<telegram_api::PhotoSize> &&size_ptr,
353                                           PhotoFormat format) {
354   CHECK(size_ptr != nullptr);
355 
356   string type;
357   PhotoSize res;
358   BufferSlice content;
359   switch (size_ptr->get_id()) {
360     case telegram_api::photoSizeEmpty::ID:
361       return std::move(res);
362     case telegram_api::photoSize::ID: {
363       auto size = move_tl_object_as<telegram_api::photoSize>(size_ptr);
364 
365       type = std::move(size->type_);
366       res.dimensions = get_dimensions(size->w_, size->h_, "photoSize");
367       res.size = size->size_;
368 
369       break;
370     }
371     case telegram_api::photoCachedSize::ID: {
372       auto size = move_tl_object_as<telegram_api::photoCachedSize>(size_ptr);
373 
374       type = std::move(size->type_);
375       CHECK(size->bytes_.size() <= static_cast<size_t>(std::numeric_limits<int32>::max()));
376       res.dimensions = get_dimensions(size->w_, size->h_, "photoCachedSize");
377       res.size = static_cast<int32>(size->bytes_.size());
378 
379       content = std::move(size->bytes_);
380 
381       break;
382     }
383     case telegram_api::photoStrippedSize::ID: {
384       auto size = move_tl_object_as<telegram_api::photoStrippedSize>(size_ptr);
385       if (format != PhotoFormat::Jpeg) {
386         LOG(ERROR) << "Receive unexpected JPEG minithumbnail in photo " << id << " from " << source << " of format "
387                    << format;
388         return std::move(res);
389       }
390       return size->bytes_.as_slice().str();
391     }
392     case telegram_api::photoSizeProgressive::ID: {
393       auto size = move_tl_object_as<telegram_api::photoSizeProgressive>(size_ptr);
394 
395       if (size->sizes_.empty()) {
396         LOG(ERROR) << "Receive photo " << id << " from " << source << " with empty size " << to_string(size);
397         return std::move(res);
398       }
399       std::sort(size->sizes_.begin(), size->sizes_.end());
400 
401       type = std::move(size->type_);
402       res.dimensions = get_dimensions(size->w_, size->h_, "photoSizeProgressive");
403       res.size = size->sizes_.back();
404       size->sizes_.pop_back();
405       res.progressive_sizes = std::move(size->sizes_);
406 
407       break;
408     }
409     case telegram_api::photoPathSize::ID: {
410       auto size = move_tl_object_as<telegram_api::photoPathSize>(size_ptr);
411       if (format != PhotoFormat::Tgs && format != PhotoFormat::Webp) {
412         LOG(ERROR) << "Receive unexpected SVG minithumbnail in photo " << id << " from " << source << " of format "
413                    << format;
414         return std::move(res);
415       }
416       return size->bytes_.as_slice().str();
417     }
418     default:
419       UNREACHABLE();
420       break;
421   }
422 
423   if (type.size() != 1) {
424     LOG(ERROR) << "Wrong photoSize \"" << type << "\" " << res;
425     res.type = 0;
426   } else {
427     res.type = static_cast<uint8>(type[0]);
428     if (res.type >= 128) {
429       LOG(ERROR) << "Wrong photoSize \"" << type << "\" " << res;
430       res.type = 0;
431     }
432   }
433   if (source.get_type("get_photo_size") == PhotoSizeSource::Type::Thumbnail) {
434     source.thumbnail().thumbnail_type = res.type;
435   }
436 
437   res.file_id = register_photo(file_manager, source, id, access_hash, std::move(file_reference), owner_dialog_id,
438                                res.size, dc_id, format);
439 
440   if (!content.empty()) {
441     file_manager->set_content(res.file_id, std::move(content));
442   }
443 
444   return std::move(res);
445 }
446 
get_animation_size(FileManager * file_manager,PhotoSizeSource source,int64 id,int64 access_hash,std::string file_reference,DcId dc_id,DialogId owner_dialog_id,tl_object_ptr<telegram_api::videoSize> && size)447 AnimationSize get_animation_size(FileManager *file_manager, PhotoSizeSource source, int64 id, int64 access_hash,
448                                  std::string file_reference, DcId dc_id, DialogId owner_dialog_id,
449                                  tl_object_ptr<telegram_api::videoSize> &&size) {
450   CHECK(size != nullptr);
451   AnimationSize res;
452   if (size->type_ != "v" && size->type_ != "u") {
453     LOG(ERROR) << "Wrong videoSize \"" << size->type_ << "\" in " << to_string(size);
454   }
455   res.type = static_cast<uint8>(size->type_[0]);
456   if (res.type >= 128) {
457     LOG(ERROR) << "Wrong videoSize \"" << res.type << "\" " << res;
458     res.type = 0;
459   }
460   res.dimensions = get_dimensions(size->w_, size->h_, "get_animation_size");
461   res.size = size->size_;
462   if ((size->flags_ & telegram_api::videoSize::VIDEO_START_TS_MASK) != 0) {
463     res.main_frame_timestamp = size->video_start_ts_;
464   }
465 
466   if (source.get_type("get_animation_size") == PhotoSizeSource::Type::Thumbnail) {
467     source.thumbnail().thumbnail_type = res.type;
468   }
469 
470   res.file_id = register_photo(file_manager, source, id, access_hash, std::move(file_reference), owner_dialog_id,
471                                res.size, dc_id, PhotoFormat::Mpeg4);
472   return res;
473 }
474 
get_web_document_photo_size(FileManager * file_manager,FileType file_type,DialogId owner_dialog_id,tl_object_ptr<telegram_api::WebDocument> web_document_ptr)475 PhotoSize get_web_document_photo_size(FileManager *file_manager, FileType file_type, DialogId owner_dialog_id,
476                                       tl_object_ptr<telegram_api::WebDocument> web_document_ptr) {
477   if (web_document_ptr == nullptr) {
478     return {};
479   }
480 
481   FileId file_id;
482   vector<tl_object_ptr<telegram_api::DocumentAttribute>> attributes;
483   int32 size = 0;
484   string mime_type;
485   switch (web_document_ptr->get_id()) {
486     case telegram_api::webDocument::ID: {
487       auto web_document = move_tl_object_as<telegram_api::webDocument>(web_document_ptr);
488       auto r_http_url = parse_url(web_document->url_);
489       if (r_http_url.is_error()) {
490         LOG(ERROR) << "Can't parse URL " << web_document->url_;
491         return {};
492       }
493       auto http_url = r_http_url.move_as_ok();
494       auto url = http_url.get_url();
495       file_id = file_manager->register_remote(FullRemoteFileLocation(file_type, url, web_document->access_hash_),
496                                               FileLocationSource::FromServer, owner_dialog_id, 0, web_document->size_,
497                                               get_url_query_file_name(http_url.query_));
498       size = web_document->size_;
499       mime_type = std::move(web_document->mime_type_);
500       attributes = std::move(web_document->attributes_);
501       break;
502     }
503     case telegram_api::webDocumentNoProxy::ID: {
504       auto web_document = move_tl_object_as<telegram_api::webDocumentNoProxy>(web_document_ptr);
505       if (web_document->url_.find('.') == string::npos) {
506         LOG(ERROR) << "Receive invalid URL " << web_document->url_;
507         return {};
508       }
509 
510       auto r_file_id = file_manager->from_persistent_id(web_document->url_, file_type);
511       if (r_file_id.is_error()) {
512         LOG(ERROR) << "Can't register URL: " << r_file_id.error();
513         return {};
514       }
515       file_id = r_file_id.move_as_ok();
516 
517       size = web_document->size_;
518       mime_type = std::move(web_document->mime_type_);
519       attributes = std::move(web_document->attributes_);
520       break;
521     }
522     default:
523       UNREACHABLE();
524   }
525   CHECK(file_id.is_valid());
526   bool is_animation = mime_type == "video/mp4";
527   bool is_gif = mime_type == "image/gif";
528 
529   Dimensions dimensions;
530   for (auto &attribute : attributes) {
531     switch (attribute->get_id()) {
532       case telegram_api::documentAttributeImageSize::ID: {
533         auto image_size = move_tl_object_as<telegram_api::documentAttributeImageSize>(attribute);
534         dimensions = get_dimensions(image_size->w_, image_size->h_, "web documentAttributeImageSize");
535         break;
536       }
537       case telegram_api::documentAttributeAnimated::ID:
538       case telegram_api::documentAttributeHasStickers::ID:
539       case telegram_api::documentAttributeSticker::ID:
540       case telegram_api::documentAttributeVideo::ID:
541       case telegram_api::documentAttributeAudio::ID:
542         LOG(ERROR) << "Unexpected web document attribute " << to_string(attribute);
543         break;
544       case telegram_api::documentAttributeFilename::ID:
545         break;
546       default:
547         UNREACHABLE();
548     }
549   }
550 
551   PhotoSize s;
552   s.type = is_animation ? 'v' : (is_gif ? 'g' : (file_type == FileType::Thumbnail ? 't' : 'n'));
553   s.dimensions = dimensions;
554   s.size = size;
555   s.file_id = file_id;
556   return s;
557 }
558 
get_thumbnail_object(FileManager * file_manager,const PhotoSize & photo_size,PhotoFormat format)559 td_api::object_ptr<td_api::thumbnail> get_thumbnail_object(FileManager *file_manager, const PhotoSize &photo_size,
560                                                            PhotoFormat format) {
561   if (!photo_size.file_id.is_valid()) {
562     return nullptr;
563   }
564 
565   if (format == PhotoFormat::Jpeg && photo_size.type == 'g') {
566     format = PhotoFormat::Gif;
567   }
568 
569   return td_api::make_object<td_api::thumbnail>(get_thumbnail_format_object(format), photo_size.dimensions.width,
570                                                 photo_size.dimensions.height,
571                                                 file_manager->get_file_object(photo_size.file_id));
572 }
573 
get_photo_size_object(FileManager * file_manager,const PhotoSize * photo_size)574 static tl_object_ptr<td_api::photoSize> get_photo_size_object(FileManager *file_manager, const PhotoSize *photo_size) {
575   if (photo_size == nullptr || !photo_size->file_id.is_valid()) {
576     return nullptr;
577   }
578 
579   return td_api::make_object<td_api::photoSize>(
580       photo_size->type ? std::string(1, static_cast<char>(photo_size->type))
581                        : std::string(),  // TODO replace string type with integer type
582       file_manager->get_file_object(photo_size->file_id), photo_size->dimensions.width, photo_size->dimensions.height,
583       vector<int32>(photo_size->progressive_sizes));
584 }
585 
get_photo_sizes_object(FileManager * file_manager,const vector<PhotoSize> & photo_sizes)586 static vector<td_api::object_ptr<td_api::photoSize>> get_photo_sizes_object(FileManager *file_manager,
587                                                                             const vector<PhotoSize> &photo_sizes) {
588   auto sizes = transform(photo_sizes, [file_manager](const PhotoSize &photo_size) {
589     return get_photo_size_object(file_manager, &photo_size);
590   });
591   std::stable_sort(sizes.begin(), sizes.end(), [](const auto &lhs, const auto &rhs) {
592     if (lhs->photo_->expected_size_ != rhs->photo_->expected_size_) {
593       return lhs->photo_->expected_size_ < rhs->photo_->expected_size_;
594     }
595     return static_cast<uint32>(lhs->width_) * static_cast<uint32>(lhs->height_) <
596            static_cast<uint32>(rhs->width_) * static_cast<uint32>(rhs->height_);
597   });
598   td::remove_if(sizes, [](const auto &size) {
599     return !size->photo_->local_->can_be_downloaded_ && !size->photo_->local_->is_downloading_completed_;
600   });
601   return sizes;
602 }
603 
operator ==(const PhotoSize & lhs,const PhotoSize & rhs)604 bool operator==(const PhotoSize &lhs, const PhotoSize &rhs) {
605   return lhs.type == rhs.type && lhs.dimensions == rhs.dimensions && lhs.size == rhs.size &&
606          lhs.file_id == rhs.file_id && lhs.progressive_sizes == rhs.progressive_sizes;
607 }
608 
operator !=(const PhotoSize & lhs,const PhotoSize & rhs)609 bool operator!=(const PhotoSize &lhs, const PhotoSize &rhs) {
610   return !(lhs == rhs);
611 }
612 
operator <(const PhotoSize & lhs,const PhotoSize & rhs)613 bool operator<(const PhotoSize &lhs, const PhotoSize &rhs) {
614   if (lhs.size != rhs.size) {
615     return lhs.size < rhs.size;
616   }
617   auto lhs_pixels = get_pixel_count(lhs.dimensions);
618   auto rhs_pixels = get_pixel_count(rhs.dimensions);
619   if (lhs_pixels != rhs_pixels) {
620     return lhs_pixels < rhs_pixels;
621   }
622   int32 lhs_type = lhs.type == 't' ? -1 : lhs.type;
623   int32 rhs_type = rhs.type == 't' ? -1 : rhs.type;
624   if (lhs_type != rhs_type) {
625     return lhs_type < rhs_type;
626   }
627   if (lhs.file_id != rhs.file_id) {
628     return lhs.file_id.get() < rhs.file_id.get();
629   }
630   return lhs.dimensions.width < rhs.dimensions.width;
631 }
632 
operator <<(StringBuilder & string_builder,const PhotoSize & photo_size)633 StringBuilder &operator<<(StringBuilder &string_builder, const PhotoSize &photo_size) {
634   return string_builder << "{type = " << photo_size.type << ", dimensions = " << photo_size.dimensions
635                         << ", size = " << photo_size.size << ", file_id = " << photo_size.file_id
636                         << ", progressive_sizes = " << photo_size.progressive_sizes << "}";
637 }
638 
get_animated_chat_photo_object(FileManager * file_manager,const AnimationSize * animation_size)639 static tl_object_ptr<td_api::animatedChatPhoto> get_animated_chat_photo_object(FileManager *file_manager,
640                                                                                const AnimationSize *animation_size) {
641   if (animation_size == nullptr || !animation_size->file_id.is_valid()) {
642     return nullptr;
643   }
644 
645   return td_api::make_object<td_api::animatedChatPhoto>(animation_size->dimensions.width,
646                                                         file_manager->get_file_object(animation_size->file_id),
647                                                         animation_size->main_frame_timestamp);
648 }
649 
operator ==(const AnimationSize & lhs,const AnimationSize & rhs)650 bool operator==(const AnimationSize &lhs, const AnimationSize &rhs) {
651   return static_cast<const PhotoSize &>(lhs) == static_cast<const PhotoSize &>(rhs) &&
652          fabs(lhs.main_frame_timestamp - rhs.main_frame_timestamp) < 1e-3;
653 }
654 
operator !=(const AnimationSize & lhs,const AnimationSize & rhs)655 bool operator!=(const AnimationSize &lhs, const AnimationSize &rhs) {
656   return !(lhs == rhs);
657 }
658 
operator <<(StringBuilder & string_builder,const AnimationSize & animation_size)659 StringBuilder &operator<<(StringBuilder &string_builder, const AnimationSize &animation_size) {
660   return string_builder << static_cast<const PhotoSize &>(animation_size) << " from "
661                         << animation_size.main_frame_timestamp;
662 }
663 
get_encrypted_file_photo(FileManager * file_manager,unique_ptr<EncryptedFile> && file,tl_object_ptr<secret_api::decryptedMessageMediaPhoto> && photo,DialogId owner_dialog_id)664 Photo get_encrypted_file_photo(FileManager *file_manager, unique_ptr<EncryptedFile> &&file,
665                                tl_object_ptr<secret_api::decryptedMessageMediaPhoto> &&photo,
666                                DialogId owner_dialog_id) {
667   FileId file_id = file_manager->register_remote(
668       FullRemoteFileLocation(FileType::Encrypted, file->id_, file->access_hash_, DcId::create(file->dc_id_), string()),
669       FileLocationSource::FromServer, owner_dialog_id, photo->size_, 0,
670       PSTRING() << static_cast<uint64>(file->id_) << ".jpg");
671   file_manager->set_encryption_key(file_id, FileEncryptionKey{photo->key_.as_slice(), photo->iv_.as_slice()});
672 
673   Photo res;
674   res.id = 0;
675   res.date = 0;
676 
677   if (!photo->thumb_.empty()) {
678     res.photos.push_back(get_secret_thumbnail_photo_size(file_manager, std::move(photo->thumb_), owner_dialog_id,
679                                                          photo->thumb_w_, photo->thumb_h_));
680   }
681 
682   PhotoSize s;
683   s.type = 'i';
684   s.dimensions = get_dimensions(photo->w_, photo->h_, "get_encrypted_file_photo");
685   s.size = photo->size_;
686   s.file_id = file_id;
687   res.photos.push_back(s);
688 
689   return res;
690 }
691 
get_photo(FileManager * file_manager,tl_object_ptr<telegram_api::Photo> && photo,DialogId owner_dialog_id)692 Photo get_photo(FileManager *file_manager, tl_object_ptr<telegram_api::Photo> &&photo, DialogId owner_dialog_id) {
693   if (photo == nullptr || photo->get_id() == telegram_api::photoEmpty::ID) {
694     return Photo();
695   }
696   CHECK(photo->get_id() == telegram_api::photo::ID);
697   return get_photo(file_manager, move_tl_object_as<telegram_api::photo>(photo), owner_dialog_id);
698 }
699 
get_photo(FileManager * file_manager,tl_object_ptr<telegram_api::photo> && photo,DialogId owner_dialog_id)700 Photo get_photo(FileManager *file_manager, tl_object_ptr<telegram_api::photo> &&photo, DialogId owner_dialog_id) {
701   CHECK(photo != nullptr);
702   Photo res;
703 
704   res.id = photo->id_;
705   res.date = photo->date_;
706   res.has_stickers = photo->has_stickers_;
707 
708   if (res.is_empty()) {
709     LOG(ERROR) << "Receive photo with identifier " << res.id.get();
710     res.id = -3;
711   }
712 
713   DcId dc_id = DcId::create(photo->dc_id_);
714   for (auto &size_ptr : photo->sizes_) {
715     auto photo_size = get_photo_size(file_manager, PhotoSizeSource::thumbnail(FileType::Photo, 0), photo->id_,
716                                      photo->access_hash_, photo->file_reference_.as_slice().str(), dc_id,
717                                      owner_dialog_id, std::move(size_ptr), PhotoFormat::Jpeg);
718     if (photo_size.get_offset() == 0) {
719       PhotoSize &size = photo_size.get<0>();
720       if (size.type == 0 || size.type == 't' || size.type == 'i' || size.type == 'u' || size.type == 'v') {
721         LOG(ERROR) << "Skip unallowed photo size " << size;
722         continue;
723       }
724       res.photos.push_back(std::move(size));
725     } else {
726       res.minithumbnail = std::move(photo_size.get<1>());
727     }
728   }
729 
730   for (auto &size_ptr : photo->video_sizes_) {
731     auto animation = get_animation_size(file_manager, PhotoSizeSource::thumbnail(FileType::Photo, 0), photo->id_,
732                                         photo->access_hash_, photo->file_reference_.as_slice().str(), dc_id,
733                                         owner_dialog_id, std::move(size_ptr));
734     if (animation.type != 0 && animation.dimensions.width == animation.dimensions.height) {
735       res.animations.push_back(std::move(animation));
736     }
737   }
738 
739   return res;
740 }
741 
get_web_document_photo(FileManager * file_manager,tl_object_ptr<telegram_api::WebDocument> web_document,DialogId owner_dialog_id)742 Photo get_web_document_photo(FileManager *file_manager, tl_object_ptr<telegram_api::WebDocument> web_document,
743                              DialogId owner_dialog_id) {
744   PhotoSize s = get_web_document_photo_size(file_manager, FileType::Photo, owner_dialog_id, std::move(web_document));
745   Photo photo;
746   if (s.file_id.is_valid() && s.type != 'v' && s.type != 'g') {
747     photo.id = 0;
748     photo.photos.push_back(s);
749   }
750   return photo;
751 }
752 
get_photo_object(FileManager * file_manager,const Photo & photo)753 tl_object_ptr<td_api::photo> get_photo_object(FileManager *file_manager, const Photo &photo) {
754   if (photo.is_empty()) {
755     return nullptr;
756   }
757 
758   return td_api::make_object<td_api::photo>(photo.has_stickers, get_minithumbnail_object(photo.minithumbnail),
759                                             get_photo_sizes_object(file_manager, photo.photos));
760 }
761 
get_chat_photo_object(FileManager * file_manager,const Photo & photo)762 tl_object_ptr<td_api::chatPhoto> get_chat_photo_object(FileManager *file_manager, const Photo &photo) {
763   if (photo.is_empty()) {
764     return nullptr;
765   }
766 
767   const AnimationSize *animation = photo.animations.empty() ? nullptr : &photo.animations.back();
768   return td_api::make_object<td_api::chatPhoto>(
769       photo.id.get(), photo.date, get_minithumbnail_object(photo.minithumbnail),
770       get_photo_sizes_object(file_manager, photo.photos), get_animated_chat_photo_object(file_manager, animation));
771 }
772 
photo_delete_thumbnail(Photo & photo)773 void photo_delete_thumbnail(Photo &photo) {
774   for (size_t i = 0; i < photo.photos.size(); i++) {
775     if (photo.photos[i].type == 't') {
776       photo.photos.erase(photo.photos.begin() + i);
777       return;
778     }
779   }
780 }
781 
photo_has_input_media(FileManager * file_manager,const Photo & photo,bool is_secret,bool is_bot)782 bool photo_has_input_media(FileManager *file_manager, const Photo &photo, bool is_secret, bool is_bot) {
783   if (photo.photos.empty() || photo.photos.back().type != 'i') {
784     LOG(ERROR) << "Wrong photo: " << photo;
785     return false;
786   }
787   auto file_id = photo.photos.back().file_id;
788   auto file_view = file_manager->get_file_view(file_id);
789   if (is_secret) {
790     if (!file_view.is_encrypted_secret() || !file_view.has_remote_location()) {
791       return false;
792     }
793 
794     for (const auto &size : photo.photos) {
795       if (size.type == 't' && size.file_id.is_valid()) {
796         return false;
797       }
798     }
799 
800     return true;
801   } else {
802     if (file_view.is_encrypted()) {
803       return false;
804     }
805     if (is_bot && file_view.has_remote_location()) {
806       return true;
807     }
808     return /* file_view.has_remote_location() || */ file_view.has_url();
809   }
810 }
811 
photo_get_input_media(FileManager * file_manager,const Photo & photo,tl_object_ptr<telegram_api::InputFile> input_file,int32 ttl)812 tl_object_ptr<telegram_api::InputMedia> photo_get_input_media(FileManager *file_manager, const Photo &photo,
813                                                               tl_object_ptr<telegram_api::InputFile> input_file,
814                                                               int32 ttl) {
815   if (!photo.photos.empty()) {
816     auto file_id = photo.photos.back().file_id;
817     auto file_view = file_manager->get_file_view(file_id);
818     if (file_view.is_encrypted()) {
819       return nullptr;
820     }
821     if (file_view.has_remote_location() && !file_view.main_remote_location().is_web() && input_file == nullptr) {
822       int32 flags = 0;
823       if (ttl != 0) {
824         flags |= telegram_api::inputMediaPhoto::TTL_SECONDS_MASK;
825       }
826       return make_tl_object<telegram_api::inputMediaPhoto>(flags, file_view.main_remote_location().as_input_photo(),
827                                                            ttl);
828     }
829     if (file_view.has_url()) {
830       int32 flags = 0;
831       if (ttl != 0) {
832         flags |= telegram_api::inputMediaPhotoExternal::TTL_SECONDS_MASK;
833       }
834       LOG(INFO) << "Create inputMediaPhotoExternal with a URL " << file_view.url() << " and TTL " << ttl;
835       return make_tl_object<telegram_api::inputMediaPhotoExternal>(flags, file_view.url(), ttl);
836     }
837     if (input_file == nullptr) {
838       CHECK(!file_view.has_remote_location());
839     }
840   }
841   if (input_file != nullptr) {
842     int32 flags = 0;
843     vector<tl_object_ptr<telegram_api::InputDocument>> added_stickers;
844     if (photo.has_stickers) {
845       flags |= telegram_api::inputMediaUploadedPhoto::STICKERS_MASK;
846       added_stickers = file_manager->get_input_documents(photo.sticker_file_ids);
847     }
848     if (ttl != 0) {
849       flags |= telegram_api::inputMediaUploadedPhoto::TTL_SECONDS_MASK;
850     }
851 
852     return make_tl_object<telegram_api::inputMediaUploadedPhoto>(flags, std::move(input_file),
853                                                                  std::move(added_stickers), ttl);
854   }
855   return nullptr;
856 }
857 
photo_get_secret_input_media(FileManager * file_manager,const Photo & photo,tl_object_ptr<telegram_api::InputEncryptedFile> input_file,const string & caption,BufferSlice thumbnail)858 SecretInputMedia photo_get_secret_input_media(FileManager *file_manager, const Photo &photo,
859                                               tl_object_ptr<telegram_api::InputEncryptedFile> input_file,
860                                               const string &caption, BufferSlice thumbnail) {
861   FileId file_id;
862   int32 width = 0;
863   int32 height = 0;
864 
865   FileId thumbnail_file_id;
866   int32 thumbnail_width = 0;
867   int32 thumbnail_height = 0;
868   for (const auto &size : photo.photos) {
869     if (size.type == 'i') {
870       file_id = size.file_id;
871       width = size.dimensions.width;
872       height = size.dimensions.height;
873     }
874     if (size.type == 't') {
875       thumbnail_file_id = size.file_id;
876       thumbnail_width = size.dimensions.width;
877       thumbnail_height = size.dimensions.height;
878     }
879   }
880   if (file_id.empty()) {
881     LOG(ERROR) << "NO SIZE";
882     return {};
883   }
884   auto file_view = file_manager->get_file_view(file_id);
885   auto &encryption_key = file_view.encryption_key();
886   if (!file_view.is_encrypted_secret() || encryption_key.empty()) {
887     return {};
888   }
889   if (file_view.has_remote_location()) {
890     LOG(INFO) << "Photo has remote location";
891     input_file = file_view.main_remote_location().as_input_encrypted_file();
892   }
893   if (input_file == nullptr) {
894     return {};
895   }
896   if (thumbnail_file_id.is_valid() && thumbnail.empty()) {
897     return {};
898   }
899 
900   return SecretInputMedia{
901       std::move(input_file),
902       make_tl_object<secret_api::decryptedMessageMediaPhoto>(
903           std::move(thumbnail), thumbnail_width, thumbnail_height, width, height, narrow_cast<int32>(file_view.size()),
904           BufferSlice(encryption_key.key_slice()), BufferSlice(encryption_key.iv_slice()), caption)};
905 }
906 
photo_get_file_ids(const Photo & photo)907 vector<FileId> photo_get_file_ids(const Photo &photo) {
908   auto result = transform(photo.photos, [](auto &size) { return size.file_id; });
909   if (!photo.animations.empty()) {
910     // photo file IDs must be first
911     append(result, transform(photo.animations, [](auto &size) { return size.file_id; }));
912   }
913   return result;
914 }
915 
operator ==(const Photo & lhs,const Photo & rhs)916 bool operator==(const Photo &lhs, const Photo &rhs) {
917   return lhs.id.get() == rhs.id.get() && lhs.photos == rhs.photos && lhs.animations == rhs.animations;
918 }
919 
operator !=(const Photo & lhs,const Photo & rhs)920 bool operator!=(const Photo &lhs, const Photo &rhs) {
921   return !(lhs == rhs);
922 }
923 
operator <<(StringBuilder & string_builder,const Photo & photo)924 StringBuilder &operator<<(StringBuilder &string_builder, const Photo &photo) {
925   string_builder << "[ID = " << photo.id.get() << ", photos = " << format::as_array(photo.photos);
926   if (!photo.animations.empty()) {
927     string_builder << ", animations = " << format::as_array(photo.animations);
928   }
929   return string_builder << ']';
930 }
931 
convert_photo_to_profile_photo(const tl_object_ptr<telegram_api::photo> & photo)932 tl_object_ptr<telegram_api::userProfilePhoto> convert_photo_to_profile_photo(
933     const tl_object_ptr<telegram_api::photo> &photo) {
934   if (photo == nullptr) {
935     return nullptr;
936   }
937 
938   bool have_photo_small = false;
939   bool have_photo_big = false;
940   for (auto &size_ptr : photo->sizes_) {
941     switch (size_ptr->get_id()) {
942       case telegram_api::photoSizeEmpty::ID:
943         break;
944       case telegram_api::photoSize::ID: {
945         auto size = static_cast<const telegram_api::photoSize *>(size_ptr.get());
946         if (size->type_ == "a") {
947           have_photo_small = true;
948         } else if (size->type_ == "c") {
949           have_photo_big = true;
950         }
951         break;
952       }
953       case telegram_api::photoCachedSize::ID: {
954         auto size = static_cast<const telegram_api::photoCachedSize *>(size_ptr.get());
955         if (size->type_ == "a") {
956           have_photo_small = true;
957         } else if (size->type_ == "c") {
958           have_photo_big = true;
959         }
960         break;
961       }
962       case telegram_api::photoStrippedSize::ID:
963         break;
964       case telegram_api::photoSizeProgressive::ID: {
965         auto size = static_cast<const telegram_api::photoSizeProgressive *>(size_ptr.get());
966         if (size->type_ == "a") {
967           have_photo_small = true;
968         } else if (size->type_ == "c") {
969           have_photo_big = true;
970         }
971         break;
972       }
973       default:
974         UNREACHABLE();
975         break;
976     }
977   }
978   if (!have_photo_small || !have_photo_big) {
979     return nullptr;
980   }
981   bool has_video = !photo->video_sizes_.empty();
982   return make_tl_object<telegram_api::userProfilePhoto>(0, has_video, photo->id_, BufferSlice(), photo->dc_id_);
983 }
984 
985 }  // namespace td
986