2023-05-18 16:20:22 +02:00
|
|
|
//
|
|
|
|
// Copyright Aliaksei Levin (levlam@telegram.org), Arseny Smirnov (arseny30@gmail.com) 2014-2023
|
|
|
|
//
|
|
|
|
// Distributed under the Boost Software License, Version 1.0. (See accompanying
|
|
|
|
// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
|
|
|
|
//
|
|
|
|
#include "td/telegram/StoryContent.h"
|
|
|
|
|
|
|
|
#include "td/telegram/DocumentsManager.h"
|
|
|
|
#include "td/telegram/files/FileId.h"
|
2023-05-23 15:05:04 +02:00
|
|
|
#include "td/telegram/files/FileManager.h"
|
|
|
|
#include "td/telegram/files/FileType.h"
|
2023-05-18 16:20:22 +02:00
|
|
|
#include "td/telegram/Photo.h"
|
2023-05-23 15:05:04 +02:00
|
|
|
#include "td/telegram/StickersManager.h"
|
2023-05-18 16:20:22 +02:00
|
|
|
#include "td/telegram/Td.h"
|
|
|
|
#include "td/telegram/VideosManager.h"
|
|
|
|
|
|
|
|
#include "td/utils/common.h"
|
|
|
|
|
2023-06-05 13:33:00 +02:00
|
|
|
#include <cmath>
|
|
|
|
|
2023-05-18 16:20:22 +02:00
|
|
|
namespace td {
|
|
|
|
|
|
|
|
class StoryContentPhoto final : public StoryContent {
|
|
|
|
public:
|
|
|
|
Photo photo_;
|
|
|
|
|
|
|
|
StoryContentPhoto() = default;
|
|
|
|
explicit StoryContentPhoto(Photo &&photo) : photo_(std::move(photo)) {
|
|
|
|
}
|
|
|
|
|
|
|
|
StoryContentType get_type() const final {
|
|
|
|
return StoryContentType::Photo;
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
class StoryContentVideo final : public StoryContent {
|
|
|
|
public:
|
|
|
|
FileId file_id_;
|
|
|
|
FileId alt_file_id_;
|
|
|
|
|
|
|
|
StoryContentVideo() = default;
|
|
|
|
StoryContentVideo(FileId file_id, FileId alt_file_id) : file_id_(file_id), alt_file_id_(alt_file_id) {
|
|
|
|
}
|
|
|
|
|
|
|
|
StoryContentType get_type() const final {
|
|
|
|
return StoryContentType::Video;
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
class StoryContentUnsupported final : public StoryContent {
|
|
|
|
public:
|
|
|
|
static constexpr int32 CURRENT_VERSION = 1;
|
|
|
|
int32 version_ = CURRENT_VERSION;
|
|
|
|
|
|
|
|
StoryContentUnsupported() = default;
|
|
|
|
explicit StoryContentUnsupported(int32 version) : version_(version) {
|
|
|
|
}
|
|
|
|
|
|
|
|
StoryContentType get_type() const final {
|
|
|
|
return StoryContentType::Unsupported;
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
unique_ptr<StoryContent> get_story_content(Td *td, tl_object_ptr<telegram_api::MessageMedia> &&media_ptr,
|
|
|
|
DialogId owner_dialog_id) {
|
|
|
|
CHECK(media_ptr != nullptr);
|
|
|
|
int32 constructor_id = media_ptr->get_id();
|
|
|
|
switch (constructor_id) {
|
|
|
|
case telegram_api::messageMediaPhoto::ID: {
|
|
|
|
auto media = move_tl_object_as<telegram_api::messageMediaPhoto>(media_ptr);
|
|
|
|
if (media->photo_ == nullptr || (media->flags_ & telegram_api::messageMediaPhoto::TTL_SECONDS_MASK) != 0 ||
|
|
|
|
media->spoiler_) {
|
2023-05-24 14:46:53 +02:00
|
|
|
LOG(ERROR) << "Receive a story with content " << to_string(media);
|
2023-05-18 16:20:22 +02:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
2023-05-25 00:26:26 +02:00
|
|
|
auto photo = get_photo(td, std::move(media->photo_), owner_dialog_id, FileType::PhotoStory);
|
2023-05-18 16:20:22 +02:00
|
|
|
if (photo.is_empty()) {
|
2023-05-24 14:46:53 +02:00
|
|
|
LOG(ERROR) << "Receive a story with empty photo";
|
2023-05-18 16:20:22 +02:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
return make_unique<StoryContentPhoto>(std::move(photo));
|
|
|
|
}
|
|
|
|
case telegram_api::messageMediaDocument::ID: {
|
|
|
|
auto media = move_tl_object_as<telegram_api::messageMediaDocument>(media_ptr);
|
|
|
|
if (media->document_ == nullptr || (media->flags_ & telegram_api::messageMediaDocument::TTL_SECONDS_MASK) != 0 ||
|
|
|
|
media->spoiler_) {
|
2023-05-24 14:46:53 +02:00
|
|
|
LOG(ERROR) << "Receive a story with content " << to_string(media);
|
2023-05-18 16:20:22 +02:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
auto document_ptr = std::move(media->document_);
|
|
|
|
int32 document_id = document_ptr->get_id();
|
|
|
|
if (document_id == telegram_api::documentEmpty::ID) {
|
2023-05-24 14:46:53 +02:00
|
|
|
LOG(ERROR) << "Receive a story with empty document";
|
2023-05-18 16:20:22 +02:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
CHECK(document_id == telegram_api::document::ID);
|
|
|
|
auto parsed_document = td->documents_manager_->on_get_document(
|
2023-05-25 13:19:54 +02:00
|
|
|
move_tl_object_as<telegram_api::document>(document_ptr), owner_dialog_id, nullptr, Document::Type::Video,
|
|
|
|
DocumentsManager::Subtype::Story);
|
2023-05-18 16:20:22 +02:00
|
|
|
if (parsed_document.empty() || parsed_document.type != Document::Type::Video) {
|
2023-05-24 14:46:53 +02:00
|
|
|
LOG(ERROR) << "Receive a story with " << parsed_document;
|
2023-05-18 16:20:22 +02:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
CHECK(parsed_document.file_id.is_valid());
|
|
|
|
|
|
|
|
FileId alt_file_id;
|
|
|
|
if (media->alt_document_ != nullptr) {
|
|
|
|
auto alt_document_ptr = std::move(media->alt_document_);
|
|
|
|
int32 alt_document_id = alt_document_ptr->get_id();
|
|
|
|
if (alt_document_id == telegram_api::documentEmpty::ID) {
|
|
|
|
LOG(ERROR) << "Receive alternative " << to_string(alt_document_ptr);
|
|
|
|
} else {
|
|
|
|
CHECK(alt_document_id == telegram_api::document::ID);
|
|
|
|
auto parsed_alt_document = td->documents_manager_->on_get_document(
|
2023-05-25 13:19:54 +02:00
|
|
|
move_tl_object_as<telegram_api::document>(alt_document_ptr), owner_dialog_id, nullptr,
|
|
|
|
Document::Type::Video, DocumentsManager::Subtype::Story);
|
2023-05-18 16:20:22 +02:00
|
|
|
if (parsed_alt_document.empty() || parsed_alt_document.type != Document::Type::Video) {
|
|
|
|
LOG(ERROR) << "Receive alternative " << to_string(alt_document_ptr);
|
|
|
|
} else {
|
|
|
|
alt_file_id = parsed_alt_document.file_id;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return make_unique<StoryContentVideo>(parsed_document.file_id, alt_file_id);
|
|
|
|
}
|
|
|
|
case telegram_api::messageMediaUnsupported::ID:
|
|
|
|
return make_unique<StoryContentUnsupported>();
|
|
|
|
default:
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
return nullptr;
|
|
|
|
}
|
|
|
|
|
2023-05-23 15:05:04 +02:00
|
|
|
Result<unique_ptr<StoryContent>> get_input_story_content(
|
|
|
|
Td *td, td_api::object_ptr<td_api::InputStoryContent> &&input_story_content, DialogId owner_dialog_id) {
|
|
|
|
LOG(INFO) << "Get input story content from " << to_string(input_story_content);
|
2023-05-25 20:25:16 +02:00
|
|
|
if (input_story_content == nullptr) {
|
|
|
|
return Status::Error(400, "Input story content must be non-empty");
|
|
|
|
}
|
2023-05-23 15:05:04 +02:00
|
|
|
|
|
|
|
switch (input_story_content->get_id()) {
|
|
|
|
case td_api::inputStoryContentPhoto::ID: {
|
|
|
|
auto input_story = static_cast<const td_api::inputStoryContentPhoto *>(input_story_content.get());
|
|
|
|
TRY_RESULT(file_id, td->file_manager_->get_input_file_id(FileType::Photo, input_story->photo_, owner_dialog_id,
|
|
|
|
false, false));
|
2023-05-25 00:26:26 +02:00
|
|
|
file_id =
|
|
|
|
td->file_manager_->copy_file_id(file_id, FileType::PhotoStory, owner_dialog_id, "get_input_story_content");
|
2023-05-23 15:05:04 +02:00
|
|
|
auto sticker_file_ids =
|
|
|
|
td->stickers_manager_->get_attached_sticker_file_ids(input_story->added_sticker_file_ids_);
|
|
|
|
TRY_RESULT(photo,
|
|
|
|
create_photo(td->file_manager_.get(), file_id, PhotoSize(), 720, 1280, std::move(sticker_file_ids)));
|
|
|
|
return make_unique<StoryContentPhoto>(std::move(photo));
|
|
|
|
}
|
|
|
|
case td_api::inputStoryContentVideo::ID: {
|
|
|
|
auto input_story = static_cast<const td_api::inputStoryContentVideo *>(input_story_content.get());
|
|
|
|
TRY_RESULT(file_id, td->file_manager_->get_input_file_id(FileType::Video, input_story->video_, owner_dialog_id,
|
|
|
|
false, false));
|
2023-05-25 13:19:54 +02:00
|
|
|
file_id =
|
|
|
|
td->file_manager_->copy_file_id(file_id, FileType::VideoStory, owner_dialog_id, "get_input_story_content");
|
2023-06-05 13:33:00 +02:00
|
|
|
if (input_story->duration_ < 0 || input_story->duration_ > 60.0) {
|
|
|
|
return Status::Error(400, "Invalid video duration specified");
|
|
|
|
}
|
2023-05-23 15:05:04 +02:00
|
|
|
auto sticker_file_ids =
|
|
|
|
td->stickers_manager_->get_attached_sticker_file_ids(input_story->added_sticker_file_ids_);
|
|
|
|
bool has_stickers = !sticker_file_ids.empty();
|
|
|
|
td->videos_manager_->create_video(file_id, string(), PhotoSize(), AnimationSize(), has_stickers,
|
2023-06-05 13:33:00 +02:00
|
|
|
std::move(sticker_file_ids), "story.mp4", "video/mp4",
|
|
|
|
static_cast<int32>(std::ceil(input_story->duration_)), input_story->duration_,
|
2023-05-23 15:05:04 +02:00
|
|
|
get_dimensions(720, 1280, nullptr), true, 0, false);
|
|
|
|
|
|
|
|
return make_unique<StoryContentVideo>(file_id, FileId());
|
|
|
|
}
|
|
|
|
default:
|
2023-05-24 18:35:45 +02:00
|
|
|
UNREACHABLE();
|
|
|
|
return nullptr;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
telegram_api::object_ptr<telegram_api::InputMedia> get_story_content_input_media(
|
|
|
|
Td *td, const StoryContent *content, telegram_api::object_ptr<telegram_api::InputFile> input_file) {
|
|
|
|
switch (content->get_type()) {
|
|
|
|
case StoryContentType::Photo: {
|
|
|
|
const auto *story_content = static_cast<const StoryContentPhoto *>(content);
|
|
|
|
return photo_get_input_media(td->file_manager_.get(), story_content->photo_, std::move(input_file), 0, false);
|
|
|
|
}
|
|
|
|
case StoryContentType::Video: {
|
|
|
|
const auto *story_content = static_cast<const StoryContentVideo *>(content);
|
|
|
|
return td->videos_manager_->get_input_media(story_content->file_id_, std::move(input_file), nullptr, 0, false);
|
|
|
|
}
|
|
|
|
case StoryContentType::Unsupported:
|
|
|
|
default:
|
2023-05-23 15:05:04 +02:00
|
|
|
UNREACHABLE();
|
|
|
|
return nullptr;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-06-02 14:54:43 +02:00
|
|
|
void compare_story_contents(const StoryContent *old_content, const StoryContent *new_content, bool &is_content_changed,
|
|
|
|
bool &need_update) {
|
|
|
|
StoryContentType content_type = new_content->get_type();
|
|
|
|
if (old_content->get_type() != content_type) {
|
|
|
|
need_update = true;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
switch (content_type) {
|
|
|
|
case StoryContentType::Photo: {
|
|
|
|
const auto *old_ = static_cast<const StoryContentPhoto *>(old_content);
|
|
|
|
const auto *new_ = static_cast<const StoryContentPhoto *>(new_content);
|
|
|
|
if (old_->photo_ != new_->photo_) {
|
|
|
|
need_update = true;
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
case StoryContentType::Video: {
|
|
|
|
const auto *old_ = static_cast<const StoryContentVideo *>(old_content);
|
|
|
|
const auto *new_ = static_cast<const StoryContentVideo *>(new_content);
|
|
|
|
if (old_->file_id_ != new_->file_id_ || old_->alt_file_id_ != new_->alt_file_id_) {
|
|
|
|
need_update = true;
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
case StoryContentType::Unsupported: {
|
|
|
|
const auto *old_ = static_cast<const StoryContentUnsupported *>(old_content);
|
|
|
|
const auto *new_ = static_cast<const StoryContentUnsupported *>(new_content);
|
|
|
|
if (old_->version_ != new_->version_) {
|
|
|
|
is_content_changed = true;
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
default:
|
|
|
|
UNREACHABLE();
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-05-18 16:20:22 +02:00
|
|
|
void merge_story_contents(Td *td, const StoryContent *old_content, StoryContent *new_content, DialogId dialog_id,
|
2023-06-13 11:46:31 +02:00
|
|
|
bool &is_content_changed, bool &need_update) {
|
2023-05-18 16:20:22 +02:00
|
|
|
StoryContentType content_type = new_content->get_type();
|
|
|
|
CHECK(old_content->get_type() == content_type);
|
|
|
|
|
|
|
|
switch (content_type) {
|
|
|
|
case StoryContentType::Photo: {
|
|
|
|
const auto *old_ = static_cast<const StoryContentPhoto *>(old_content);
|
|
|
|
auto *new_ = static_cast<StoryContentPhoto *>(new_content);
|
2023-06-13 11:46:31 +02:00
|
|
|
merge_photos(td, &old_->photo_, &new_->photo_, dialog_id, false, is_content_changed, need_update);
|
2023-05-18 16:20:22 +02:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
case StoryContentType::Video: {
|
|
|
|
const auto *old_ = static_cast<const StoryContentVideo *>(old_content);
|
|
|
|
const auto *new_ = static_cast<const StoryContentVideo *>(new_content);
|
2023-06-13 11:46:31 +02:00
|
|
|
if (old_->file_id_ != new_->file_id_ || old_->alt_file_id_ != new_->alt_file_id_) {
|
2023-05-18 16:20:22 +02:00
|
|
|
need_update = true;
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
case StoryContentType::Unsupported: {
|
|
|
|
const auto *old_ = static_cast<const StoryContentUnsupported *>(old_content);
|
|
|
|
const auto *new_ = static_cast<const StoryContentUnsupported *>(new_content);
|
|
|
|
if (old_->version_ != new_->version_) {
|
|
|
|
is_content_changed = true;
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
default:
|
|
|
|
UNREACHABLE();
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-05-29 19:48:31 +02:00
|
|
|
unique_ptr<StoryContent> copy_story_content(const StoryContent *content) {
|
|
|
|
if (content == nullptr) {
|
|
|
|
return nullptr;
|
|
|
|
}
|
|
|
|
switch (content->get_type()) {
|
|
|
|
case StoryContentType::Photo: {
|
|
|
|
const auto *story_content = static_cast<const StoryContentPhoto *>(content);
|
|
|
|
return make_unique<StoryContentPhoto>(Photo(story_content->photo_));
|
|
|
|
}
|
|
|
|
case StoryContentType::Video: {
|
|
|
|
const auto *story_content = static_cast<const StoryContentVideo *>(content);
|
|
|
|
return make_unique<StoryContentVideo>(story_content->file_id_, story_content->alt_file_id_);
|
|
|
|
}
|
|
|
|
case StoryContentType::Unsupported: {
|
|
|
|
const auto *story_content = static_cast<const StoryContentUnsupported *>(content);
|
|
|
|
return make_unique<StoryContentUnsupported>(story_content->version_);
|
|
|
|
}
|
|
|
|
default:
|
|
|
|
UNREACHABLE();
|
|
|
|
return nullptr;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-05-29 22:00:32 +02:00
|
|
|
unique_ptr<StoryContent> dup_story_content(Td *td, const StoryContent *content) {
|
|
|
|
if (content == nullptr) {
|
|
|
|
return nullptr;
|
|
|
|
}
|
|
|
|
|
|
|
|
auto fix_file_id = [file_manager = td->file_manager_.get()](FileId file_id) {
|
|
|
|
return file_manager->dup_file_id(file_id, "dup_story_content");
|
|
|
|
};
|
|
|
|
|
|
|
|
switch (content->get_type()) {
|
|
|
|
case StoryContentType::Photo: {
|
|
|
|
const auto *old_content = static_cast<const StoryContentPhoto *>(content);
|
|
|
|
// Find 'i' or largest
|
|
|
|
PhotoSize photo_size;
|
|
|
|
for (const auto &size : old_content->photo_.photos) {
|
|
|
|
if (size.type == 'i') {
|
|
|
|
photo_size = size;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (photo_size.type == 0) {
|
|
|
|
for (const auto &size : old_content->photo_.photos) {
|
|
|
|
if (photo_size.type == 0 || photo_size < size) {
|
|
|
|
photo_size = size;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
photo_size.type = 'i';
|
|
|
|
photo_size.file_id = fix_file_id(photo_size.file_id);
|
|
|
|
|
|
|
|
auto result = make_unique<StoryContentPhoto>();
|
|
|
|
result->photo_.photos.push_back(std::move(photo_size));
|
|
|
|
return std::move(result);
|
|
|
|
}
|
|
|
|
case StoryContentType::Video: {
|
|
|
|
const auto *old_content = static_cast<const StoryContentVideo *>(content);
|
|
|
|
return make_unique<StoryContentVideo>(
|
|
|
|
td->videos_manager_->dup_video(fix_file_id(old_content->file_id_), old_content->file_id_), FileId());
|
|
|
|
}
|
|
|
|
case StoryContentType::Unsupported:
|
|
|
|
return nullptr;
|
|
|
|
default:
|
|
|
|
UNREACHABLE();
|
|
|
|
return nullptr;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-05-19 12:41:15 +02:00
|
|
|
td_api::object_ptr<td_api::StoryContent> get_story_content_object(Td *td, const StoryContent *content) {
|
|
|
|
CHECK(content != nullptr);
|
|
|
|
switch (content->get_type()) {
|
|
|
|
case StoryContentType::Photo: {
|
|
|
|
const auto *s = static_cast<const StoryContentPhoto *>(content);
|
|
|
|
auto photo = get_photo_object(td->file_manager_.get(), s->photo_);
|
|
|
|
if (photo == nullptr) {
|
|
|
|
return td_api::make_object<td_api::storyContentUnsupported>();
|
|
|
|
}
|
|
|
|
return td_api::make_object<td_api::storyContentPhoto>(std::move(photo));
|
|
|
|
}
|
|
|
|
case StoryContentType::Video: {
|
|
|
|
const auto *s = static_cast<const StoryContentVideo *>(content);
|
2023-06-05 13:51:32 +02:00
|
|
|
return td_api::make_object<td_api::storyContentVideo>(
|
|
|
|
td->videos_manager_->get_story_video_object(s->file_id_),
|
|
|
|
td->videos_manager_->get_story_video_object(s->alt_file_id_));
|
2023-05-19 12:41:15 +02:00
|
|
|
}
|
|
|
|
case StoryContentType::Unsupported:
|
|
|
|
return td_api::make_object<td_api::storyContentUnsupported>();
|
|
|
|
default:
|
|
|
|
UNREACHABLE();
|
|
|
|
return nullptr;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-05-24 16:24:36 +02:00
|
|
|
FileId get_story_content_any_file_id(const Td *td, const StoryContent *content) {
|
|
|
|
switch (content->get_type()) {
|
|
|
|
case StoryContentType::Photo:
|
|
|
|
return get_photo_any_file_id(static_cast<const StoryContentPhoto *>(content)->photo_);
|
|
|
|
case StoryContentType::Video:
|
|
|
|
return static_cast<const StoryContentVideo *>(content)->file_id_;
|
|
|
|
case StoryContentType::Unsupported:
|
|
|
|
default:
|
|
|
|
return {};
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-05-19 16:00:33 +02:00
|
|
|
vector<FileId> get_story_content_file_ids(const Td *td, const StoryContent *content) {
|
|
|
|
switch (content->get_type()) {
|
|
|
|
case StoryContentType::Photo:
|
|
|
|
return photo_get_file_ids(static_cast<const StoryContentPhoto *>(content)->photo_);
|
|
|
|
case StoryContentType::Video: {
|
|
|
|
vector<FileId> result;
|
|
|
|
const auto *s = static_cast<const StoryContentVideo *>(content);
|
|
|
|
Document(Document::Type::Video, s->file_id_).append_file_ids(td, result);
|
|
|
|
Document(Document::Type::Video, s->alt_file_id_).append_file_ids(td, result);
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
case StoryContentType::Unsupported:
|
|
|
|
default:
|
|
|
|
return {};
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-05-24 15:23:05 +02:00
|
|
|
int32 get_story_content_duration(const Td *td, const StoryContent *content) {
|
|
|
|
CHECK(content != nullptr);
|
|
|
|
switch (content->get_type()) {
|
|
|
|
case StoryContentType::Video: {
|
|
|
|
auto file_id = static_cast<const StoryContentVideo *>(content)->file_id_;
|
|
|
|
return td->videos_manager_->get_video_duration(file_id);
|
|
|
|
}
|
|
|
|
default:
|
|
|
|
return -1;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-05-18 16:20:22 +02:00
|
|
|
} // namespace td
|