/* This file is part of Telegram Desktop, the official desktop application for the Telegram messaging service. For license and copyright information please follow this link: https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "data/components/ephemeral_messages.h" #include "api/api_common.h" #include "api/api_text_entities.h" #include "apiwrap.h" #include "base/random.h" #include "base/unixtime.h" #include "data/components/welcome_messages.h" #include "data/data_channel.h" #include "data/data_chat.h" #include "data/data_forum_topic.h" #include "data/data_histories.h" #include "data/data_peer_bot_command.h" #include "data/data_session.h" #include "data/data_user.h" #include "history/view/history_view_element.h" #include "history/history.h" #include "history/history_item.h" #include "history/history_item_edition.h" #include "iv/iv_rich_message_serializer.h" #include "iv/iv_rich_page.h" #include "lang/lang_keys.h" #include "main/main_session.h" #include "ui/toast/toast.h" namespace Data { namespace { constexpr auto kKeepDuration = TimeId(2 * 86400); constexpr auto kPruneEach = 3600 * crl::time(1000); constexpr auto kPendingFlushDelay = 2 * crl::time(1000); struct ParsedCommand { QString command; QString username; }; [[nodiscard]] std::optional ParseCommand( const QString &text) { if (!text.startsWith(QChar('/'))) { return std::nullopt; } const auto size = int(text.size()); const auto good = [](QChar ch) { return ch.isLetterOrNumber() || (ch == QChar('_')); }; auto i = 1; while (i < size && good(text[i])) { ++i; } auto result = ParsedCommand{ .command = text.mid(1, i - 1) }; if (result.command.isEmpty()) { return std::nullopt; } if (i < size && text[i] == QChar('@')) { const auto from = ++i; while (i < size && good(text[i])) { ++i; } result.username = text.mid(from, i - from); if (result.username.isEmpty()) { return std::nullopt; } } if (i < size && !text[i].isSpace()) { return std::nullopt; } return result; } [[nodiscard]] HistoryMessageContent ContentFromEphemeral( not_null session, not_null item, const MTPDephemeralMessage &data) { auto result = HistoryMessageContent{ .text = { qs(data.vmessage()), Api::EntitiesFromMTP( session, data.ventities().value_or_empty()), }, .media = (data.vmedia() ? HistoryItem::CreateMedia(item, *data.vmedia()) : nullptr), .markup = HistoryMessageMarkupData(data.vreply_markup()), .invertMedia = data.is_invert_media(), .hideEdited = true, }; if (const auto richMessage = data.vrich_message()) { result.richPage = Iv::ParseRichPage(session, *richMessage); } return result; } [[nodiscard]] bool SupportsEphemeral(not_null peer) { if (const auto user = peer->asUser()) { return user->isBot(); } return peer->isChat() || peer->isMegagroup(); } template [[nodiscard]] Value TakeHint( base::flat_map< not_null, base::flat_map> &map, not_null history, PeerId key) { const auto i = map.find(history); if (i == end(map)) { return Value(); } const auto result = i->second.take(key); if (i->second.empty()) { map.erase(i); } return result.value_or(Value()); } } // namespace PeerId PeerIdFromEphemeral(const MTPDephemeralMessage &data) { const auto peer = data.vpeer_id(); const auto peerId = peer ? peerFromMTP(*peer) : PeerId(); if (peerId && !peerIsUser(peerId)) { return peerId; } return data.is_out() ? peerFromUser(UserId(data.vreceiver_id())) : peerFromMTP(data.vfrom_id()); } EphemeralMessages::EphemeralMessages(not_null session) : _session(session) , _pruneTimer([=] { pruneOld(); }) , _pendingTimer([=] { drainPending(true); }) { _session->data().itemRemoved( ) | rpl::on_next([=](not_null item) { if (item->isEphemeral() || anchored(item)) { itemRemoved(item); } }, _lifetime); _pruneTimer.callEach(kPruneEach); } EphemeralMessages::~EphemeralMessages() = default; void EphemeralMessages::clear() { _lifetime.destroy(); _convertLocalTarget = {}; base::take(_data); base::take(_anchored); base::take(_pending); base::take(_callbackTopicHints); } void EphemeralMessages::apply(const MTPDupdateNewEphemeralMessage &update) { if (update.vmessage().data().is_welcome_template()) { _session->welcomeMessages().applyNew(update.vmessage().data()); return; } applyOrDefer(update.vmessage()); } void EphemeralMessages::applyOrDefer(const MTPEphemeralMessage &message) { if (replyTargetMissing(message.data())) { _pending.push_back(message); _pendingTimer.callOnce(kPendingFlushDelay); return; } applyNew(message.data()); drainPending(); } bool EphemeralMessages::replyTargetMissing( const MTPDephemeralMessage &data) const { const auto header = data.vreply_to(); if (!header) { return false; } return header->match([&](const MTPDmessageReplyHeader &reply) { const auto replyToId = reply.vreply_to_msg_id(); if (!reply.is_reply_to_ephemeral() || !replyToId) { return false; } const auto peerId = PeerIdFromEphemeral(data); if (!peerId) { return false; } const auto history = _session->data().history(peerId); return !lookupItem(history->peer, replyToId->v); }, [](const MTPDmessageReplyStoryHeader &) { return false; }); } bool EphemeralMessages::mentionsMe( not_null history, const MTPDephemeralMessage &data) const { if (data.is_out()) { return false; } const auto text = qs(data.vmessage()); if (const auto entities = data.ventities()) { for (const auto &entity : entities->v) { auto found = false; entity.match([&](const MTPDmessageEntityMentionName &data) { found = (UserId(data.vuser_id()) == _session->userId()); }, [&](const MTPDmessageEntityMention &data) { const auto tag = text.mid( data.voffset().v + 1, data.vlength().v - 1); found = ranges::any_of( _session->user()->usernames(), [&](const QString &username) { return !tag.compare(username, Qt::CaseInsensitive); }); }, [](const auto &) { }); if (found) { return true; } } } const auto header = data.vreply_to(); if (!header) { return false; } return header->match([&](const MTPDmessageReplyHeader &reply) { const auto replyToId = reply.vreply_to_msg_id(); if (!replyToId) { return false; } const auto item = reply.is_reply_to_ephemeral() ? lookupItem(history->peer, replyToId->v) : _session->data().message(history->peer->id, replyToId->v); return (item != nullptr) && item->out(); }, [](const MTPDmessageReplyStoryHeader &) { return false; }); } void EphemeralMessages::drainPending(bool force) { while (!_pending.empty()) { const auto i = ranges::find_if(_pending, [&]( const MTPEphemeralMessage &message) { return force || !replyTargetMissing(message.data()); }); if (i == end(_pending)) { return; } const auto message = *i; _pending.erase(i); applyNew(message.data()); } } void EphemeralMessages::apply(const MTPDupdateEditEphemeralMessage &update) { const auto &data = update.vmessage().data(); if (data.is_welcome_template()) { _session->welcomeMessages().applyEdit(data); return; } const auto peerId = PeerIdFromEphemeral(data); if (!peerId) { return; } const auto history = _session->data().history(peerId); const auto item = lookupItem(history->peer, data.vid().v); if (!item) { applyNew(data); return; } else if (anchored(item)) { item->applyContent(ContentFromEphemeral(_session, item, data)); return; } auto edition = HistoryMessageEdition(); edition.isEditHide = true; edition.editDate = -1; edition.useSameViews = true; edition.useSameForwards = true; edition.useSameReplies = true; edition.useSameReactions = true; edition.useSameSuggest = true; edition.textWithEntities = { qs(data.vmessage()), Api::EntitiesFromMTP( _session, data.ventities().value_or_empty()), }; edition.replyMarkup = HistoryMessageMarkupData(data.vreply_markup()); edition.mtpMedia = data.vmedia(); edition.invertMedia = data.is_invert_media(); if (const auto richMessage = data.vrich_message()) { edition.richPage = Iv::ParseRichPage(_session, *richMessage); } item->applyEdition(std::move(edition)); } void EphemeralMessages::apply( const MTPDupdateDeleteEphemeralMessages &update) { const auto history = _session->data().historyLoaded( peerFromMTP(update.vpeer())); if (!history) { return; } auto items = std::vector>(); for (const auto &id : update.vids().v) { if (_session->welcomeMessages().applyDelete(history->peer, id.v)) { continue; } if (const auto item = lookupItem(history->peer, id.v)) { if (anchored(item)) { revertAnchored(item); } else { items.push_back(item); } } } _session->data().destroyMessagesWithCacheCleanup(items); } HistoryItem *EphemeralMessages::applyNew(const MTPDephemeralMessage &data) { const auto peerId = PeerIdFromEphemeral(data); if (!peerId) { return nullptr; } const auto history = _session->data().history(peerId); const auto ephemeralId = data.vid().v; if (const auto existing = lookupItem(history->peer, ephemeralId)) { return existing; } if (const auto anchorMsgId = data.vanchor_msg_id()) { const auto result = applyAnchored(history, data, anchorMsgId->v); if (result) { return result; } } const auto fromId = peerFromMTP(data.vfrom_id()); auto replyTo = FullReplyTo(); if (const auto topMsgId = data.vtop_msg_id()) { replyTo.topicRootId = topMsgId->v; } if (const auto header = data.vreply_to()) { header->match([&](const MTPDmessageReplyHeader &reply) { if (const auto top = reply.vreply_to_top_id()) { if (!replyTo.topicRootId) { replyTo.topicRootId = top->v; } } const auto replyToId = reply.vreply_to_msg_id(); if (reply.is_reply_to_ephemeral() && replyToId) { const auto target = lookupItem(history->peer, replyToId->v); if (target) { replyTo.messageId = target->fullId(); if (!replyTo.topicRootId) { replyTo.topicRootId = target->topicRootId(); } } } else if (replyToId) { if (!replyTo.topicRootId) { replyTo.topicRootId = replyToId->v; } replyTo.messageId = { history->peer->id, replyTo.topicRootId, }; } }, [](const MTPDmessageReplyStoryHeader &) { }); } if (!replyTo.topicRootId && !replyTo.messageId && !data.is_out() && fromId) { if (const auto hinted = takeCallbackTopic(history, fromId)) { replyTo.topicRootId = hinted; replyTo.messageId = { history->peer->id, hinted }; } } if (_convertLocalTarget && data.is_out() && (data.vmedia() || data.vrich_message())) { const auto localId = base::take(_convertLocalTarget); if (const auto local = _session->data().message(localId)) { if (local->isEphemeral()) { local->updateSentContent({ qs(data.vmessage()), Api::EntitiesFromMTP( _session, data.ventities().value_or_empty()) }, data.vmedia(), data.vrich_message()); local->markEphemeralSent(); registerEntry( history, ephemeralId, UserId(data.vreceiver_id()), local); recountAttachToPrevious(local); _session->data().requestItemResize(local); return local; } } } const auto mentioned = mentionsMe(history, data); const auto item = history->addNewLocalMessage( { .id = _session->data().nextLocalMessageId(), .flags = (MessageFlag::HistoryEntry | MessageFlag::Ephemeral | (data.is_out() ? MessageFlag::Outgoing : MessageFlag()) | (data.is_invert_media() ? MessageFlag::InvertMedia : MessageFlag()) | (data.is_noforwards() ? MessageFlag::NoForwards : MessageFlag()) | (fromId ? MessageFlag::HasFromId : MessageFlag()) | (replyTo.messageId ? MessageFlag::HasReplyInfo : MessageFlag()) | (data.vreply_markup() ? MessageFlag::HasReplyMarkup : MessageFlag()) | (mentioned ? MessageFlag::MentionsMe : MessageFlag())), .from = fromId, .replyTo = replyTo, .date = data.vdate().v, .markup = HistoryMessageMarkupData(data.vreply_markup()), }, TextWithEntities { qs(data.vmessage()), Api::EntitiesFromMTP( _session, data.ventities().value_or_empty()), }, data.vmedia() ? *data.vmedia() : MTPMessageMedia(MTP_messageMediaEmpty())); if (const auto richMessage = data.vrich_message()) { item->applyLocalRichPage(Iv::ParseRichPage(_session, *richMessage)); } registerEntry(history, ephemeralId, UserId(data.vreceiver_id()), item); recountAttachToPrevious(item); _session->data().requestItemResize(item); return item; } HistoryItem *EphemeralMessages::applyAnchored( not_null history, const MTPDephemeralMessage &data, MsgId anchorMsgId) { const auto item = _session->data().message( history->peer->id, anchorMsgId); if (!item || !item->isRegular() || item->isService() || item->isEphemeral() || item->groupId() || item->isEditingMedia()) { return nullptr; } const auto fullId = item->fullId(); if (!_anchored.contains(fullId)) { _anchored.emplace(fullId, item->backupContent()); } unregisterEntry(item); item->applyContent(ContentFromEphemeral(_session, item, data)); registerEntry(history, data.vid().v, UserId(data.vreceiver_id()), item); _session->data().requestItemResize(item); return item; } void EphemeralMessages::registerEntry( not_null history, int32 ephemeralId, UserId receiverId, not_null item) { _data[history].push_back({ .ephemeralId = ephemeralId, .receiverId = receiverId, .item = item, }); } void EphemeralMessages::unregisterEntry(not_null item) { const auto i = _data.find(item->history()); if (i == end(_data)) { return; } i->second.erase( ranges::remove(i->second, item.get(), &Entry::item), end(i->second)); if (i->second.empty()) { _data.erase(i); } } void EphemeralMessages::revertAnchored(not_null item) { auto original = _anchored.take(item->fullId()); if (!original) { return; } unregisterEntry(item); item->applyContent(std::move(*original)); } void EphemeralMessages::recountAttachToPrevious( not_null item) { const auto view = item->mainView(); if (!view) { return; } view->previousInBlocksChanged(); if (const auto next = view->nextInBlocks()) { next->previousInBlocksChanged(); } } HistoryItem *EphemeralMessages::lookupItem( not_null peer, int32 ephemeralId) const { const auto history = _session->data().historyLoaded(peer); if (!history) { return nullptr; } const auto i = _data.find(history); if (i == end(_data)) { return nullptr; } const auto j = ranges::find( i->second, ephemeralId, &Entry::ephemeralId); return (j != end(i->second)) ? j->item.get() : nullptr; } int32 EphemeralMessages::lookupId(not_null item) const { const auto entry = findByItem(item); return entry ? entry->ephemeralId : 0; } UserId EphemeralMessages::receiverId( not_null item) const { const auto entry = findByItem(item); return entry ? entry->receiverId : UserId(); } bool EphemeralMessages::anchored(not_null item) const { return !_anchored.empty() && _anchored.contains(item->fullId()); } const Media *EphemeralMessages::anchoredMedia( not_null item) const { const auto i = _anchored.find(item->fullId()); return (i != end(_anchored)) ? i->second.media.get() : nullptr; } UserData *EphemeralMessages::replyReceiver( not_null item) const { if (const auto entry = findByItem(item)) { return botForSending(*entry); } if (item->out() && item->isEphemeral()) { const auto target = _session->data().message( item->replyTo().messageId); if (target && target->isEphemeral() && !target->out()) { const auto from = target->from(); return from ? from->asUser() : nullptr; } } return nullptr; } UserData *EphemeralMessages::replyBot( not_null target) const { if (!target->isEphemeral() || target->out()) { return nullptr; } const auto entry = findByItem(target); return entry ? botForSending(*entry) : nullptr; } bool EphemeralMessages::wouldSend(const Api::MessageToSend &message) const { const auto history = message.action.history; const auto peer = history->peer; if (!SupportsEphemeral(peer)) { return false; } if (const auto replyToId = realReplyId(message)) { const auto replyTo = _session->data().message(replyToId); if (replyTo && replyTo->isEphemeral()) { return replyBot(replyTo) != nullptr; } } return findCommandBot(peer, message.textWithTags.text.trimmed()) != nullptr; } bool EphemeralMessages::hasEphemeralCommand( not_null peer, const QString &text) const { if (!SupportsEphemeral(peer)) { return false; } return findCommandBot(peer, text.trimmed()) != nullptr; } bool EphemeralMessages::wouldSendMedia( not_null peer, FullReplyTo replyTo, const QString &caption) const { return isEphemeralBotReply(replyTo.messageId) || hasEphemeralCommand(peer, caption); } bool EphemeralMessages::isEphemeralBotReply(FullMsgId replyToId) const { const auto target = _session->data().message(replyToId); return target && (replyBot(target) != nullptr); } FullMsgId EphemeralMessages::realReplyId( const Api::MessageToSend &message) const { const auto &replyTo = message.action.replyTo; const auto id = replyTo.messageId; if (!id || (replyTo.topicRootId && id.msg == replyTo.topicRootId)) { return FullMsgId(); } return id; } bool EphemeralMessages::trySend(const Api::MessageToSend &message) { const auto history = message.action.history; const auto peer = history->peer; if (!SupportsEphemeral(peer)) { return false; } else if (message.action.options.scheduled || message.action.options.shortcutId) { const auto replyToId = realReplyId(message); const auto replyTo = replyToId ? _session->data().message(replyToId) : nullptr; if (wouldSend(message) || (replyTo && replyTo->isEphemeral())) { LOG(("API Error: " "Dropping a scheduled ephemeral message send.")); return true; } return false; } auto text = TextWithEntities{ message.textWithTags.text, TextUtilities::ConvertTextTagsToEntities(message.textWithTags.tags), }; TextUtilities::Trim(text); if (text.text.isEmpty()) { return false; } auto realReply = FullReplyTo(); if (const auto replyToId = realReplyId(message)) { const auto replyTo = _session->data().message(replyToId); if (replyTo && replyTo->isEphemeral()) { if (replyTo->out()) { return true; } const auto entry = findByItem(replyTo); const auto bot = entry ? botForSending(*entry) : nullptr; if (!bot) { reportDroppedReply(); return true; } send( history, bot, std::move(text), entry->ephemeralId, MsgId(), FullReplyTo(), message.webPage, message.action.options.invertCaption); return true; } realReply = message.action.replyTo; } const auto bot = findCommandBot(peer, text.text); if (!bot) { return false; } send( history, bot, std::move(text), 0, message.action.replyTo.topicRootId, realReply, message.webPage, message.action.options.invertCaption); return true; } UserData *EphemeralMessages::findCommandBot( not_null peer, const QString &text) const { if (!SupportsEphemeral(peer)) { return nullptr; } const auto parsed = ParseCommand(text); if (!parsed) { return nullptr; } const auto matches = [&]( not_null user, const std::vector &list) { if (!parsed->username.isEmpty() && parsed->username.compare( user->username(), Qt::CaseInsensitive) != 0) { return false; } return ranges::any_of(list, [&](const BotCommand &command) { return command.ephemeral && !command.command.compare( parsed->command, Qt::CaseInsensitive); }); }; if (const auto user = peer->asUser()) { const auto info = user->botInfo.get(); return (info && matches(user, info->commands)) ? user : nullptr; } const auto &commands = peer->isChat() ? peer->asChat()->botCommands() : peer->asMegagroup()->mgInfo->botCommands(); for (const auto &[userId, list] : commands) { const auto user = _session->data().userLoaded(userId); if (user && matches(user, list)) { return user; } } return nullptr; } void EphemeralMessages::send( not_null history, not_null bot, TextWithEntities text, int32 replyToEphemeralId, MsgId topicRootId, FullReplyTo realReply, Data::WebPageDraft webPage, bool invertCaption) { const auto exactWebPage = !webPage.url.isEmpty() && !webPage.removed; const auto manualWebPage = exactWebPage && webPage.manual; const auto invertMedia = (exactWebPage && webPage.invert) || invertCaption; request( history, bot, std::move(text), (manualWebPage ? Data::WebPageForMTP(webPage, true) : MTPInputMedia()), manualWebPage, replyToEphemeralId, topicRootId, realReply, FullMsgId(), Data::FileOrigin(), nullptr, invertMedia); } bool EphemeralMessages::sendMedia( not_null item, const MTPInputMedia &media, Data::FileOrigin origin, Fn rebuildMedia) { const auto history = item->history(); const auto replyTo = item->replyTo(); const auto target = _session->data().message(replyTo.messageId); if (!target || !target->isEphemeral()) { const auto bot = findCommandBot( history->peer, item->originalText().text.trimmed()); if (bot) { const auto realReply = (replyTo.messageId && !(replyTo.topicRootId && replyTo.messageId.msg == replyTo.topicRootId)) ? replyTo : FullReplyTo(); request( history, bot, item->originalText(), media, true, 0, item->topicRootId(), realReply, item->fullId(), origin, rebuildMedia, item->invertMedia()); return true; } return false; } if (!target->out()) { const auto entry = findByItem(target); const auto bot = entry ? botForSending(*entry) : nullptr; if (bot) { request( item->history(), bot, item->originalText(), media, true, entry->ephemeralId, MsgId(0), FullReplyTo(), item->fullId(), origin, rebuildMedia, item->invertMedia()); return true; } reportDroppedReply(); } _session->data().destroyMessageWithCacheCleanup(item); return true; } bool EphemeralMessages::sendRich( not_null item, const MTPInputRichMessage &richMessage, const Api::SendAction &action) { const auto history = item->history(); const auto replyTo = item->replyTo(); const auto target = _session->data().message(replyTo.messageId); const auto targetEphemeral = target && target->isEphemeral(); const auto commandBot = targetEphemeral ? nullptr : findCommandBot( history->peer, item->originalText().text.trimmed()); if (!targetEphemeral && !commandBot) { return false; } else if (action.options.scheduled || action.options.shortcutId) { LOG(("API Error: " "Dropping a scheduled ephemeral rich message send.")); _session->data().destroyMessageWithCacheCleanup(item); return true; } const auto session = _session; const auto itemId = item->fullId(); const auto rebuildRich = [=]() -> std::optional { const auto local = session->data().message(itemId); if (!local) { return std::nullopt; } const auto fullPage = local->fullRichPage(); const auto page = fullPage ? fullPage : local->richPage(); if (!page) { return std::nullopt; } auto serialized = Iv::SerializeInputRichMessage( session, *page, Iv::SerializeInputRichMessageMode::FinalSubmit); const auto success = (serialized.status == Iv::SerializeInputRichMessageStatus::Success); return (success && serialized.value) ? std::move(serialized.value) : std::nullopt; }; const auto origin = action.clearDraft ? Data::FileOrigin(Data::FileOriginCloudDraft{ .peerId = history->peer->id, .topicRootId = action.replyTo.topicRootId, .monoforumPeerId = action.replyTo.monoforumPeerId, }) : Data::FileOrigin(); if (commandBot) { const auto realReply = (replyTo.messageId && !(replyTo.topicRootId && replyTo.messageId.msg == replyTo.topicRootId)) ? replyTo : FullReplyTo(); request( history, commandBot, TextWithEntities(), MTPInputMedia(), false, 0, item->topicRootId(), realReply, item->fullId(), origin, nullptr, false, richMessage, rebuildRich); return true; } if (!target->out()) { const auto entry = findByItem(target); const auto bot = entry ? botForSending(*entry) : nullptr; if (bot) { request( history, bot, TextWithEntities(), MTPInputMedia(), false, entry->ephemeralId, MsgId(0), FullReplyTo(), item->fullId(), origin, nullptr, false, richMessage, rebuildRich); return true; } reportDroppedReply(); } _session->data().destroyMessageWithCacheCleanup(item); return true; } bool EphemeralMessages::sendSimpleMedia( not_null history, FullReplyTo replyTo, const MTPInputMedia &media) { const auto target = _session->data().message(replyTo.messageId); if (!target || !target->isEphemeral()) { return false; } if (!target->out()) { const auto entry = findByItem(target); const auto bot = entry ? botForSending(*entry) : nullptr; if (bot) { request( history, bot, TextWithEntities(), media, true, entry->ephemeralId, MsgId(0)); } else { reportDroppedReply(); } } return true; } void EphemeralMessages::request( not_null history, not_null bot, TextWithEntities text, const MTPInputMedia &media, bool hasMedia, int32 replyToEphemeralId, MsgId topicRootId, FullReplyTo realReply, FullMsgId destroyOnResult, Data::FileOrigin origin, Fn rebuildMedia, bool invertMedia, std::optional richMessage, Fn()> rebuildRich) { const auto session = _session; const auto destroyLocal = [=] { if (destroyOnResult) { if (const auto local = session->data().message(destroyOnResult)) { session->data().destroyMessageWithCacheCleanup(local); } } }; const auto entities = Api::EntitiesToMTP( session, text.entities, Api::ConvertOption::SkipLocal); auto replyTo = MTPInputReplyTo(); auto hasReplyTo = false; if (replyToEphemeralId) { replyTo = MTP_inputReplyToEphemeralMessage( MTP_int(replyToEphemeralId)); hasReplyTo = true; } else if (realReply.messageId) { replyTo = Data::ReplyToForMTP(history, realReply); hasReplyTo = (replyTo.type() == mtpc_inputReplyToMessage); } else if (topicRootId && topicRootId != Data::ForumTopic::kGeneralId) { auto anchor = FullReplyTo(); anchor.messageId = { history->peer->id, topicRootId }; anchor.topicRootId = topicRootId; replyTo = Data::ReplyToForMTP(history, anchor); hasReplyTo = (replyTo.type() == mtpc_inputReplyToMessage); } using Flag = MTPephemeral_SendMessage::Flag; const auto flags = Flag::f_peer | (entities.v.isEmpty() ? Flag(0) : Flag::f_entities) | (hasMedia ? Flag::f_media : Flag(0)) | (hasReplyTo ? Flag::f_reply_to : Flag(0)) | (invertMedia ? Flag::f_invert_media : Flag(0)) | (richMessage ? Flag::f_rich_message : Flag(0)); const auto randomId = base::RandomValue(); const auto send = [=]( const auto &send, const MTPInputMedia &media, const MTPInputRichMessage &rich, int attempt) -> void { session->api().request(MTPephemeral_SendMessage( MTP_flags(flags), history->peer->input(), bot->inputUser(), MTPlong(), // query_id MTP_string(text.text), entities, media, MTPReplyMarkup(), rich, MTP_long(randomId), replyTo )).done([=](const MTPUpdates &result) { _convertLocalTarget = destroyOnResult; session->api().applyUpdates(result); if (destroyOnResult) { const auto local = session->data().message(destroyOnResult); if (local && !findByItem(local)) { session->data().destroyMessageWithCacheCleanup(local); } } }).fail([=](const MTP::Error &error) { const auto type = error.type(); const auto refreshable = !attempt && (error.code() == 400) && type.startsWith(u"FILE_REFERENCE_"_q); if (refreshable && (rebuildMedia || rebuildRich)) { session->api().refreshFileReference( origin, [=](const auto &) { if (rebuildMedia) { send(send, rebuildMedia(), rich, 1); } else if (auto rebuilt = rebuildRich()) { send(send, media, *rebuilt, 1); } else { LOG(("API Error: " "send ephemeral message - %1").arg(type)); destroyLocal(); } }); return; } LOG(("API Error: send ephemeral message - %1").arg(type)); destroyLocal(); }).send(); }; send(send, media, richMessage.value_or(MTPInputRichMessage()), 0); } void EphemeralMessages::deleteMessage(not_null item) { const auto entry = findByItem(item); if (entry && entry->receiverId) { const auto receiver = _session->data().user(entry->receiverId); _session->api().request(MTPephemeral_DeleteMessage( MTP_flags(MTPephemeral_DeleteMessage::Flag::f_peer), item->history()->peer->input(), receiver->inputUser(), MTP_int(entry->ephemeralId) )).send(); } if (anchored(item)) { revertAnchored(item); } else if (item->isEphemeral()) { _session->data().destroyMessageWithCacheCleanup(item); } } const EphemeralMessages::Entry *EphemeralMessages::findByItem( not_null item) const { const auto i = _data.find(item->history()); if (i == end(_data)) { return nullptr; } const auto j = ranges::find(i->second, item.get(), &Entry::item); return (j != end(i->second)) ? &*j : nullptr; } void EphemeralMessages::noteCallbackTopic( not_null history, PeerId botId, MsgId topicRootId) { if (!topicRootId || topicRootId == Data::ForumTopic::kGeneralId) { return; } _callbackTopicHints[history][botId] = topicRootId; } MsgId EphemeralMessages::takeCallbackTopic( not_null history, PeerId botId) { return TakeHint(_callbackTopicHints, history, botId); } UserData *EphemeralMessages::botForSending(const Entry &entry) const { if (entry.item->out()) { return _session->data().userLoaded(entry.receiverId); } const auto from = entry.item->from(); return from ? from->asUser() : nullptr; } void EphemeralMessages::reportDroppedReply() const { LOG(("API Error: Dropping an ephemeral reply without a receiver.")); Ui::Toast::Show(tr::lng_ephemeral_reply_text_only(tr::now)); } void EphemeralMessages::itemRemoved(not_null item) { _anchored.remove(item->fullId()); unregisterEntry(item); } void EphemeralMessages::pruneOld() { const auto till = base::unixtime::now() - kKeepDuration; auto old = std::vector>(); for (const auto &[history, list] : _data) { for (const auto &entry : list) { if (entry.item->date() <= till && !anchored(entry.item)) { old.push_back(entry.item); } } } _session->data().destroyMessagesWithCacheCleanup(old); } } // namespace Data