// // Copyright Aliaksei Levin (levlam@telegram.org), Arseny Smirnov (arseny30@gmail.com) 2014-2021 // // 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/DocumentsManager.h" #include "td/telegram/AnimationsManager.h" #include "td/telegram/AudiosManager.h" #include "td/telegram/AuthManager.h" #include "td/telegram/Document.h" #include "td/telegram/files/FileEncryptionKey.h" #include "td/telegram/files/FileLocation.h" #include "td/telegram/files/FileManager.h" #include "td/telegram/files/FileType.h" #include "td/telegram/misc.h" #include "td/telegram/net/DcId.h" #include "td/telegram/Photo.h" #include "td/telegram/secret_api.h" #include "td/telegram/StickersManager.h" #include "td/telegram/Td.h" #include "td/telegram/td_api.h" #include "td/telegram/telegram_api.h" #include "td/telegram/VideoNotesManager.h" #include "td/telegram/VideosManager.h" #include "td/telegram/VoiceNotesManager.h" #include "td/utils/common.h" #include "td/utils/HttpUrl.h" #include "td/utils/logging.h" #include "td/utils/MimeType.h" #include "td/utils/misc.h" #include "td/utils/PathView.h" #include "td/utils/Random.h" #include "td/utils/Slice.h" #include "td/utils/StackAllocator.h" #include "td/utils/Status.h" #include "td/utils/StringBuilder.h" #include "td/utils/utf8.h" #include namespace td { DocumentsManager::DocumentsManager(Td *td) : td_(td) { } tl_object_ptr DocumentsManager::get_document_object(FileId file_id, PhotoFormat thumbnail_format) const { if (!file_id.is_valid()) { return nullptr; } auto it = documents_.find(file_id); CHECK(it != documents_.end()); auto document = it->second.get(); CHECK(document != nullptr); return make_tl_object( document->file_name, document->mime_type, get_minithumbnail_object(document->minithumbnail), get_thumbnail_object(td_->file_manager_.get(), document->thumbnail, thumbnail_format), td_->file_manager_->get_file_object(file_id)); } Document DocumentsManager::on_get_document(RemoteDocument remote_document, DialogId owner_dialog_id, MultiPromiseActor *load_data_multipromise_ptr, Document::Type default_document_type, bool is_background, bool is_pattern) { tl_object_ptr animated; tl_object_ptr video; tl_object_ptr audio; tl_object_ptr sticker; Dimensions dimensions; string file_name; bool has_stickers = false; int32 type_attributes = 0; for (auto &attribute : remote_document.attributes) { switch (attribute->get_id()) { case telegram_api::documentAttributeImageSize::ID: { auto image_size = move_tl_object_as(attribute); dimensions = get_dimensions(image_size->w_, image_size->h_, oneline(to_string(remote_document.document)).c_str()); break; } case telegram_api::documentAttributeAnimated::ID: animated = move_tl_object_as(attribute); type_attributes++; break; case telegram_api::documentAttributeSticker::ID: sticker = move_tl_object_as(attribute); type_attributes++; break; case telegram_api::documentAttributeVideo::ID: video = move_tl_object_as(attribute); type_attributes++; break; case telegram_api::documentAttributeAudio::ID: audio = move_tl_object_as(attribute); type_attributes++; break; case telegram_api::documentAttributeFilename::ID: file_name = std::move(static_cast(attribute.get())->file_name_); break; case telegram_api::documentAttributeHasStickers::ID: has_stickers = true; break; default: UNREACHABLE(); } } int32 video_duration = 0; if (video != nullptr) { video_duration = video->duration_; auto video_dimensions = get_dimensions(video->w_, video->h_, "documentAttributeVideo"); if (dimensions.width == 0 || (video_dimensions.width != 0 && video_dimensions != dimensions)) { if (dimensions.width != 0) { LOG(ERROR) << "Receive ambiguous video dimensions " << dimensions << " and " << video_dimensions; } dimensions = video_dimensions; } if (animated != nullptr) { type_attributes--; if ((video->flags_ & telegram_api::documentAttributeVideo::ROUND_MESSAGE_MASK) != 0) { // video note without sound animated = nullptr; } else { // video animation video = nullptr; } } } if (animated != nullptr && audio != nullptr) { // animation send as audio type_attributes--; audio = nullptr; } if (animated != nullptr && sticker != nullptr) { // animation send as sticker type_attributes--; sticker = nullptr; } auto document_type = default_document_type; FileType file_type = FileType::Document; Slice default_extension; bool supports_streaming = false; PhotoFormat thumbnail_format = PhotoFormat::Jpeg; if (type_attributes == 1 || default_document_type != Document::Type::General) { // not a general document if (animated != nullptr || default_document_type == Document::Type::Animation) { document_type = Document::Type::Animation; file_type = FileType::Animation; default_extension = Slice("mp4"); } else if (audio != nullptr || default_document_type == Document::Type::Audio || default_document_type == Document::Type::VoiceNote) { bool is_voice_note = default_document_type == Document::Type::VoiceNote; if (audio != nullptr) { is_voice_note = (audio->flags_ & telegram_api::documentAttributeAudio::VOICE_MASK) != 0; } if (is_voice_note) { document_type = Document::Type::VoiceNote; file_type = FileType::VoiceNote; default_extension = Slice("oga"); file_name.clear(); } else { document_type = Document::Type::Audio; file_type = FileType::Audio; default_extension = Slice("mp3"); } } else if (sticker != nullptr || default_document_type == Document::Type::Sticker) { document_type = Document::Type::Sticker; file_type = FileType::Sticker; default_extension = Slice("webp"); owner_dialog_id = DialogId(); file_name.clear(); } else if (video != nullptr || default_document_type == Document::Type::Video || default_document_type == Document::Type::VideoNote) { bool is_video_note = default_document_type == Document::Type::VideoNote; if (video != nullptr) { is_video_note = (video->flags_ & telegram_api::documentAttributeVideo::ROUND_MESSAGE_MASK) != 0; if (!is_video_note) { supports_streaming = (video->flags_ & telegram_api::documentAttributeVideo::SUPPORTS_STREAMING_MASK) != 0; } } if (is_video_note) { document_type = Document::Type::VideoNote; file_type = FileType::VideoNote; file_name.clear(); } else { document_type = Document::Type::Video; file_type = FileType::Video; } default_extension = Slice("mp4"); } } else if (type_attributes >= 2) { LOG(WARNING) << "Receive document with more than 1 type attribute: animated = " << to_string(animated) << ", sticker = " << to_string(sticker) << ", video = " << to_string(video) << ", audio = " << to_string(audio) << ", file_name = " << file_name << ", dimensions = " << dimensions << ", has_stickers = " << has_stickers; } if (is_background) { if (document_type != Document::Type::General) { LOG(ERROR) << "Receive background of type " << document_type; document_type = Document::Type::General; } file_type = FileType::Background; if (is_pattern) { default_extension = Slice("png"); thumbnail_format = PhotoFormat::Png; } else { default_extension = Slice("jpg"); } } int64 id; int64 access_hash; int32 dc_id; int32 size; string mime_type; string file_reference; string minithumbnail; PhotoSize thumbnail; AnimationSize animated_thumbnail; FileEncryptionKey encryption_key; bool is_animated_sticker = false; bool is_web = false; bool is_web_no_proxy = false; string url; FileLocationSource source = FileLocationSource::FromServer; auto fix_animated_sticker_type = [&] { if (mime_type != "application/x-tgsticker") { return; } is_animated_sticker = true; if (document_type == Document::Type::General) { document_type = Document::Type::Sticker; file_type = FileType::Sticker; default_extension = Slice("tgs"); owner_dialog_id = DialogId(); file_name.clear(); thumbnail_format = PhotoFormat::Webp; } }; if (remote_document.document != nullptr) { auto document = std::move(remote_document.document); id = document->id_; access_hash = document->access_hash_; dc_id = document->dc_id_; size = document->size_; mime_type = std::move(document->mime_type_); file_reference = document->file_reference_.as_slice().str(); if (document_type == Document::Type::Sticker && StickersManager::has_webp_thumbnail(document->thumbs_)) { thumbnail_format = PhotoFormat::Webp; } fix_animated_sticker_type(); if (owner_dialog_id.get_type() == DialogType::SecretChat) { // secret_api::decryptedMessageMediaExternalDocument if (document_type != Document::Type::Sticker) { LOG(ERROR) << "Receive " << document_type << " in " << owner_dialog_id; return {}; } source = FileLocationSource::FromUser; } if (document_type != Document::Type::VoiceNote) { for (auto &thumb : document->thumbs_) { auto photo_size = get_photo_size(td_->file_manager_.get(), {FileType::Thumbnail, 0}, id, access_hash, file_reference, DcId::create(dc_id), owner_dialog_id, std::move(thumb), thumbnail_format); if (photo_size.get_offset() == 0) { if (!thumbnail.file_id.is_valid()) { thumbnail = std::move(photo_size.get<0>()); } } else { minithumbnail = std::move(photo_size.get<1>()); } } } for (auto &thumb : document->video_thumbs_) { if (thumb->type_ == "v") { animated_thumbnail = get_animation_size(td_->file_manager_.get(), {FileType::Thumbnail, 0}, id, access_hash, file_reference, DcId::create(dc_id), owner_dialog_id, std::move(thumb)); if (animated_thumbnail.file_id.is_valid()) { break; } } } } else if (remote_document.secret_file != nullptr) { CHECK(remote_document.secret_document != nullptr); auto file = std::move(remote_document.secret_file); auto document = std::move(remote_document.secret_document); id = file->id_; access_hash = file->access_hash_; dc_id = file->dc_id_; size = document->size_; mime_type = std::move(document->mime_type_); file_type = FileType::Encrypted; encryption_key = FileEncryptionKey{document->key_.as_slice(), document->iv_.as_slice()}; if (encryption_key.empty()) { return {}; } // do not allow encrypted animated stickers // fix_animated_sticker_type(); if (document_type != Document::Type::VoiceNote) { thumbnail = get_secret_thumbnail_photo_size(td_->file_manager_.get(), std::move(document->thumb_), owner_dialog_id, document->thumb_w_, document->thumb_h_); } } else { is_web = true; id = Random::fast(0, std::numeric_limits::max()); dc_id = 0; access_hash = 0; if (remote_document.thumbnail.type == 'v') { static_cast(animated_thumbnail) = std::move(remote_document.thumbnail); } else { thumbnail = std::move(remote_document.thumbnail); if (remote_document.thumbnail.type == 'g') { thumbnail_format = PhotoFormat::Gif; } } auto web_document_ptr = std::move(remote_document.web_document); switch (web_document_ptr->get_id()) { case telegram_api::webDocument::ID: { auto web_document = move_tl_object_as(web_document_ptr); auto r_http_url = parse_url(web_document->url_); if (r_http_url.is_error()) { LOG(ERROR) << "Can't parse URL " << web_document->url_; return {}; } auto http_url = r_http_url.move_as_ok(); access_hash = web_document->access_hash_; url = http_url.get_url(); file_name = get_url_query_file_name(http_url.query_); mime_type = std::move(web_document->mime_type_); size = web_document->size_; break; } case telegram_api::webDocumentNoProxy::ID: { is_web_no_proxy = true; auto web_document = move_tl_object_as(web_document_ptr); if (web_document->url_.find('.') == string::npos) { LOG(ERROR) << "Receive invalid URL " << web_document->url_; return {}; } url = std::move(web_document->url_); file_name = get_url_file_name(url); mime_type = std::move(web_document->mime_type_); size = web_document->size_; break; } default: UNREACHABLE(); } // do not allow web animated stickers // fix_animated_sticker_type(); } LOG(DEBUG) << "Receive document with ID = " << id << " of type " << document_type; if (!is_web && !DcId::is_valid(dc_id)) { LOG(ERROR) << "Wrong dc_id = " << dc_id; return {}; } file_name = strip_empty_characters(file_name, 255, true); auto suggested_file_name = file_name; if (suggested_file_name.empty()) { suggested_file_name = to_string(static_cast(id)); auto extension = MimeType::to_extension(mime_type, default_extension); if (!extension.empty()) { suggested_file_name += '.'; suggested_file_name += extension; } } FileId file_id; if (!is_web) { file_id = td_->file_manager_->register_remote( FullRemoteFileLocation(file_type, id, access_hash, DcId::internal(dc_id), std::move(file_reference)), source, owner_dialog_id, size, 0, suggested_file_name); if (!encryption_key.empty()) { td_->file_manager_->set_encryption_key(file_id, std::move(encryption_key)); } } else if (!is_web_no_proxy) { file_id = td_->file_manager_->register_remote(FullRemoteFileLocation(file_type, url, access_hash), source, owner_dialog_id, 0, size, file_name); } else { auto r_file_id = td_->file_manager_->from_persistent_id(url, file_type); if (r_file_id.is_error()) { LOG(ERROR) << "Can't register URL: " << r_file_id.error(); return {}; } file_id = r_file_id.move_as_ok(); } if (dimensions.width != 0 && thumbnail.dimensions.width != 0) { if ((thumbnail.dimensions.width < thumbnail.dimensions.height && dimensions.width > dimensions.height) || (thumbnail.dimensions.width > thumbnail.dimensions.height && dimensions.width < dimensions.height)) { // fix for wrong dimensions specified by the Android application std::swap(dimensions.width, dimensions.height); } } switch (document_type) { case Document::Type::Animation: td_->animations_manager_->create_animation( file_id, std::move(minithumbnail), std::move(thumbnail), std::move(animated_thumbnail), has_stickers, vector(), std::move(file_name), std::move(mime_type), video_duration, dimensions, !is_web); break; case Document::Type::Audio: { int32 duration = 0; string title; string performer; if (audio != nullptr) { duration = audio->duration_; title = std::move(audio->title_); performer = std::move(audio->performer_); } td_->audios_manager_->create_audio(file_id, std::move(minithumbnail), std::move(thumbnail), std::move(file_name), std::move(mime_type), duration, std::move(title), std::move(performer), !is_web); break; } case Document::Type::General: create_document(file_id, std::move(minithumbnail), std::move(thumbnail), std::move(file_name), std::move(mime_type), !is_web); break; case Document::Type::Sticker: if (thumbnail_format == PhotoFormat::Jpeg) { minithumbnail = string(); } td_->stickers_manager_->create_sticker(file_id, std::move(minithumbnail), std::move(thumbnail), dimensions, std::move(sticker), is_animated_sticker, load_data_multipromise_ptr); break; case Document::Type::Video: td_->videos_manager_->create_video(file_id, std::move(minithumbnail), std::move(thumbnail), std::move(animated_thumbnail), has_stickers, vector(), std::move(file_name), std::move(mime_type), video_duration, dimensions, supports_streaming, !is_web); break; case Document::Type::VideoNote: td_->video_notes_manager_->create_video_note(file_id, std::move(minithumbnail), std::move(thumbnail), video_duration, dimensions, !is_web); break; case Document::Type::VoiceNote: { int32 duration = 0; string waveform; if (audio != nullptr) { duration = audio->duration_; waveform = audio->waveform_.as_slice().str(); } td_->voice_notes_manager_->create_voice_note(file_id, std::move(mime_type), duration, std::move(waveform), !is_web); break; } case Document::Type::Unknown: default: UNREACHABLE(); } return {document_type, file_id}; } FileId DocumentsManager::on_get_document(unique_ptr new_document, bool replace) { auto file_id = new_document->file_id; CHECK(file_id.is_valid()); LOG(INFO) << "Receive document " << file_id; auto &d = documents_[new_document->file_id]; if (d == nullptr) { d = std::move(new_document); } else if (replace) { CHECK(d->file_id == new_document->file_id); if (d->mime_type != new_document->mime_type) { LOG(DEBUG) << "Document " << file_id << " mime_type has changed"; d->mime_type = new_document->mime_type; } if (d->file_name != new_document->file_name) { LOG(DEBUG) << "Document " << file_id << " file_name has changed"; d->file_name = new_document->file_name; } if (d->minithumbnail != new_document->minithumbnail) { d->minithumbnail = std::move(new_document->minithumbnail); } if (d->thumbnail != new_document->thumbnail) { if (!d->thumbnail.file_id.is_valid()) { LOG(DEBUG) << "Document " << file_id << " thumbnail has changed"; } else { LOG(INFO) << "Document " << file_id << " thumbnail has changed from " << d->thumbnail << " to " << new_document->thumbnail; } d->thumbnail = new_document->thumbnail; } } return file_id; } void DocumentsManager::create_document(FileId file_id, string minithumbnail, PhotoSize thumbnail, string file_name, string mime_type, bool replace) { auto d = make_unique(); d->file_id = file_id; d->file_name = std::move(file_name); d->mime_type = std::move(mime_type); if (!td_->auth_manager_->is_bot()) { d->minithumbnail = std::move(minithumbnail); } d->thumbnail = std::move(thumbnail); on_get_document(std::move(d), replace); } const DocumentsManager::GeneralDocument *DocumentsManager::get_document(FileId file_id) const { auto document = documents_.find(file_id); if (document == documents_.end()) { return nullptr; } CHECK(document->second->file_id == file_id); return document->second.get(); } bool DocumentsManager::has_input_media(FileId file_id, FileId thumbnail_file_id, bool is_secret) const { auto file_view = td_->file_manager_->get_file_view(file_id); if (is_secret) { if (!file_view.is_encrypted_secret() || file_view.encryption_key().empty() || !file_view.has_remote_location()) { return false; } return !thumbnail_file_id.is_valid(); } else { if (file_view.is_encrypted()) { return false; } if (td_->auth_manager_->is_bot() && file_view.has_remote_location()) { return true; } // having remote location is not enough to have InputMedia, because the file may not have valid file_reference // also file_id needs to be duped, because upload can be called to repair the file_reference and every upload // request must have unique file_id return /* file_view.has_remote_location() || */ file_view.has_url(); } } SecretInputMedia DocumentsManager::get_secret_input_media(FileId document_file_id, tl_object_ptr input_file, const string &caption, BufferSlice thumbnail) const { const GeneralDocument *document = get_document(document_file_id); CHECK(document != nullptr); auto file_view = td_->file_manager_->get_file_view(document_file_id); auto &encryption_key = file_view.encryption_key(); if (!file_view.is_encrypted_secret() || encryption_key.empty()) { return SecretInputMedia{}; } if (file_view.has_remote_location()) { input_file = file_view.main_remote_location().as_input_encrypted_file(); } if (!input_file) { return SecretInputMedia{}; } if (document->thumbnail.file_id.is_valid() && thumbnail.empty()) { return SecretInputMedia{}; } vector> attributes; if (!document->file_name.empty()) { attributes.push_back(make_tl_object(document->file_name)); } return SecretInputMedia{ std::move(input_file), make_tl_object( std::move(thumbnail), document->thumbnail.dimensions.width, document->thumbnail.dimensions.height, document->mime_type, narrow_cast(file_view.size()), BufferSlice(encryption_key.key_slice()), BufferSlice(encryption_key.iv_slice()), std::move(attributes), caption)}; } tl_object_ptr DocumentsManager::get_input_media( FileId file_id, tl_object_ptr input_file, tl_object_ptr input_thumbnail) const { auto file_view = td_->file_manager_->get_file_view(file_id); if (file_view.is_encrypted()) { return nullptr; } if (file_view.has_remote_location() && !file_view.main_remote_location().is_web() && input_file == nullptr) { return make_tl_object(0, file_view.main_remote_location().as_input_document(), 0, string()); } if (file_view.has_url()) { return make_tl_object(0, file_view.url(), 0); } if (input_file != nullptr) { const GeneralDocument *document = get_document(file_id); CHECK(document != nullptr); vector> attributes; if (!document->file_name.empty()) { attributes.push_back(make_tl_object(document->file_name)); } int32 flags = 0; if (input_thumbnail != nullptr) { flags |= telegram_api::inputMediaUploadedDocument::THUMB_MASK; } if (file_view.get_type() == FileType::DocumentAsFile) { flags |= telegram_api::inputMediaUploadedDocument::FORCE_FILE_MASK; } return make_tl_object( flags, false /*ignored*/, false /*ignored*/, std::move(input_file), std::move(input_thumbnail), document->mime_type, std::move(attributes), vector>(), 0); } else { CHECK(!file_view.has_remote_location()); } return nullptr; } FileId DocumentsManager::get_document_thumbnail_file_id(FileId file_id) const { auto document = get_document(file_id); CHECK(document != nullptr); return document->thumbnail.file_id; } void DocumentsManager::delete_document_thumbnail(FileId file_id) { auto &document = documents_[file_id]; CHECK(document != nullptr); document->thumbnail = PhotoSize(); } FileId DocumentsManager::dup_document(FileId new_id, FileId old_id) { const GeneralDocument *old_document = get_document(old_id); CHECK(old_document != nullptr); auto &new_document = documents_[new_id]; CHECK(!new_document); new_document = make_unique(*old_document); new_document->file_id = new_id; new_document->thumbnail.file_id = td_->file_manager_->dup_file_id(new_document->thumbnail.file_id); return new_id; } void DocumentsManager::merge_documents(FileId new_id, FileId old_id, bool can_delete_old) { CHECK(old_id.is_valid() && new_id.is_valid()); CHECK(new_id != old_id); LOG(INFO) << "Merge documents " << new_id << " and " << old_id; const GeneralDocument *old_ = get_document(old_id); CHECK(old_ != nullptr); auto new_it = documents_.find(new_id); if (new_it == documents_.end()) { auto &old = documents_[old_id]; if (!can_delete_old) { dup_document(new_id, old_id); } else { old->file_id = new_id; documents_.emplace(new_id, std::move(old)); } } else { GeneralDocument *new_ = new_it->second.get(); CHECK(new_ != nullptr); if (old_->thumbnail != new_->thumbnail) { // LOG_STATUS(td_->file_manager_->merge(new_->thumbnail.file_id, old_->thumbnail.file_id)); } } LOG_STATUS(td_->file_manager_->merge(new_id, old_id)); if (can_delete_old) { documents_.erase(old_id); } } void DocumentsManager::memory_stats(vector &output) { output.push_back("\"documents_\":"); output.push_back(std::to_string(documents_.size())); } string DocumentsManager::get_document_search_text(FileId file_id) const { auto document = get_document(file_id); CHECK(document); if (document->file_name.size() > 32) { return document->file_name; } auto buf = StackAllocator::alloc(256); StringBuilder sb(buf.as_slice()); auto stem = PathView(document->file_name).file_stem(); sb << document->file_name; for (size_t i = 1; i + 1 < stem.size(); i++) { if (is_utf8_character_first_code_unit(stem[i])) { sb << " " << stem.substr(0, i); } } if (sb.is_error()) { return document->file_name; } return sb.as_cslice().str(); } } // namespace td