// This is the source code of AyuGram for Desktop. // // We do not and cannot prevent the use of our code, // but be respectful and credit the original author. // // Copyright @Radolyn, 2025 #include "ayu_sync.h" #include "apiwrap.h" #include "api/api_sending.h" #include "core/application.h" #include "core/core_settings.h" #include "core/file_utilities.h" #include "data/data_document.h" #include "data/data_photo.h" #include "data/data_photo_media.h" #include "data/data_session.h" #include "history/history.h" #include "history/history_item.h" #include "main/main_session.h" #include "storage/file_download_mtproto.h" #include "storage/localimageloader.h" class TimedCountDownLatch { public: explicit TimedCountDownLatch(int count) : count_(count) { } void countDown() { std::unique_lock lock(mutex_); if (count_ > 0) { count_--; } if (count_ == 0) { cv_.notify_all(); } } bool await(std::chrono::milliseconds timeout) { std::unique_lock lock(mutex_); if (count_ == 0) { return true; } return cv_.wait_for(lock, timeout, [this] { return count_ == 0; }); } private: std::mutex mutex_; std::condition_variable cv_; int count_; }; namespace AyuSync { QString pathForSave(not_null session) { const auto path = Core::App().settings().downloadPath(); if (path.isEmpty()) { return File::DefaultDownloadPath(session); } if (path == FileDialog::Tmp()) { return session->local().tempDirectory(); } return path; } QString filePath(not_null session, const Data::Media *media) { if (const auto document = media->document()) { if (!document->filename().isEmpty()) { return pathForSave(session) + media->document()->filename(); } if (const auto name = document->filepath(true); !name.isEmpty()) { return name; } if (document->isVoiceMessage()) { return pathForSave(session) + "audio_" + QString::number(document->getDC()) + "_" + QString::number(document->id) + ".ogg"; } if (document->isVideoMessage()) { return pathForSave(session) + "round_" + QString::number(document->getDC()) + "_" + QString::number(document->id) + ".mp4"; } } else if (const auto photo = media->photo()) { return pathForSave(session) + QString::number(photo->getDC()) + "_" + QString::number(photo->id) + ".jpg"; } return QString(); } qint64 fileSize(not_null item) { if (const auto path = filePath(&item->history()->session(), item->media()); !path.isEmpty()) { QFile file(path); if (file.exists()) { auto size = file.size(); return size; } } return 0; } void loadDocuments(not_null session, const std::vector> &items) { for (const auto &item : items) { if (const auto data = item->media()->document()) { const auto size = fileSize(item); if (size == data->size) { continue; } if (size && size < data->size) { // in case there some unfinished file QFile file(filePath(session, item->media())); file.remove(); } loadDocumentSync(session, data, item); } else if (auto photo = item->media()->photo()) { if (fileSize(item) == photo->imageByteSize(Data::PhotoSize::Large)) { continue; } loadPhotoSync(session, std::pair(photo, item->fullId())); } } } void loadDocumentSync(not_null session, DocumentData *data, not_null item) { auto latch = std::make_shared(1); auto lifetime = std::make_shared(); data->save(Data::FileOriginMessage(item->fullId()), filePath(session, item->media())); rpl::single() | rpl::then( session->downloaderTaskFinished() ) | rpl::filter([&] { return data->status == FileDownloadFailed || fileSize(item) == data->size; }) | rpl::start_with_next([&]() mutable { latch->countDown(); base::take(lifetime)->destroy(); }, *lifetime); latch->await(std::chrono::minutes(5)); } void forwardMessagesSync(not_null session, const std::vector> &items, const ApiWrap::SendAction &action, Data::ForwardOptions options) { auto latch = std::make_shared(1); crl::on_main([=, &latch] { session->api().forwardMessages(Data::ResolvedForwardDraft(items, options), action, [&] { latch->countDown(); }); }); latch->await(std::chrono::minutes(1)); } void loadPhotoSync(not_null session, const std::pair, FullMsgId> &photo) { const auto folderPath = pathForSave(session); const auto downloadPath = folderPath.isEmpty() ? Core::App().settings().downloadPath() : folderPath; const auto path = downloadPath.isEmpty() ? File::DefaultDownloadPath(session) : downloadPath == FileDialog::Tmp() ? session->local().tempDirectory() : downloadPath; if (path.isEmpty()) { return; } if (!QDir().mkpath(path)) { return; } const auto view = photo.first->createMediaView(); if (!view) { return; } view->wanted(Data::PhotoSize::Large, photo.second); const auto finalCheck = [=] { return !photo.first->loading(); }; const auto saveToFiles = [=] { QDir directory(path); const auto dir = directory.absolutePath(); const auto nameBase = dir.endsWith('/') ? dir : dir + '/'; const auto fullPath = nameBase + QString::number(photo.first->getDC()) + "_" + QString::number(photo.first->id) + ".jpg"; view->saveToFile(fullPath); }; auto latch = std::make_shared(1); auto lifetime = std::make_shared(); if (finalCheck()) { saveToFiles(); } else { session->downloaderTaskFinished() | rpl::filter([&] { return finalCheck(); }) | rpl::start_with_next([&]() mutable { saveToFiles(); latch->countDown(); base::take(lifetime)->destroy(); }, *lifetime); } latch->await(std::chrono::minutes(5)); } void sendMessageSync(not_null session, Api::MessageToSend &message) { crl::on_main([=, &message] { // we cannot send events to objects // owned by a different thread // because sendMessage updates UI too session->api().sendMessage(std::move(message)); }); waitForMsgSync(session, message.action); } void waitForMsgSync(not_null session, const Api::SendAction &action) { auto latch = std::make_shared(1); auto lifetime = std::make_shared(); session->data().itemIdChanged() | rpl::filter([&](const Data::Session::IdChange &update) { return action.history->peer->id == update.newId.peer; }) | rpl::start_with_next([&] { latch->countDown(); base::take(lifetime)->destroy(); }, *lifetime); latch->await(std::chrono::minutes(2)); } void sendDocumentSync(not_null session, Ui::PreparedGroup &group, SendMediaType type, TextWithTags &&caption, const Api::SendAction &action) { const auto size = group.list.files.size(); auto latch = std::make_shared(size); auto lifetime = std::make_shared(); auto groupId = std::make_shared(); groupId->groupId = base::RandomValue(); crl::on_main([=, lst = std::move(group.list), caption = std::move(caption)]() mutable { session->api().sendFiles(std::move(lst), type, std::move(caption), groupId, action); }); // probably need to handle // session->uploader().photoFailed() // and // session->uploader().documentFailed() // too rpl::merge( session->uploader().documentReady(), session->uploader().photoReady() ) | rpl::filter([&](const Storage::UploadedMedia &docOrPhoto) { return docOrPhoto.fullId.peer == action.history->peer->id; }) | rpl::start_with_next([&] { latch->countDown(); }, *lifetime); latch->await(std::chrono::minutes(5 * size)); base::take(lifetime)->destroy(); } void sendStickerSync(not_null session, Api::MessageToSend &message, not_null document) { auto &action = message.action; crl::on_main([&] { Api::SendExistingDocument(std::move(message), document, std::nullopt); }); waitForMsgSync(session, action); } void sendVoiceSync(not_null session, const QByteArray &data, int64_t duration, bool video, const Api::SendAction &action) { crl::on_main([&] { session->api().sendVoiceMessage(data, QVector(), duration, video, action); }); waitForMsgSync(session, action); } } // namespace AyuSync