// 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, 2026 #include "ayu/utils/telegram_helpers.h" #include "apiwrap.h" #include "lang_auto.h" #include "api/api_common.h" #include "ayu/ayu_settings.h" #include "ayu/ayu_state.h" #include "ayu/ayu_worker.h" #include "ayu/data/entities.h" #include "ayu/data/messages_storage.h" #include "ayu/features/filters/filters_controller.h" #include "ayu/ui/boxes/donate_info_box.h" #include "ayu/ui/toasts.h" #include "ayu/utils/rc_manager.h" #include "core/core_settings.h" #include "core/application.h" #include "base/unixtime.h" #include "core/mime_type.h" #include "data/data_channel.h" #include "data/data_chat.h" #include "data/data_document.h" #include "data/data_forum.h" #include "data/data_forum_topic.h" #include "data/data_histories.h" #include "data/data_peer_id.h" #include "data/data_photo.h" #include "data/data_poll.h" #include "data/data_saved_sublist.h" #include "data/data_session.h" #include "data/data_user.h" #include "data/stickers/data_custom_emoji.h" #include "data/stickers/data_stickers.h" #include "history/history.h" #include "history/history_item.h" #include "history/history_item_components.h" #include "history/history_unread_things.h" #include "lang/lang_keys.h" #include "main/main_account.h" #include "main/main_domain.h" #include "main/main_session.h" #include "styles/style_ayu_styles.h" #include "styles/style_info.h" #include "ui/emoji_config.h" #include "ui/layers/generic_box.h" #include "ui/text/format_values.h" #include "ui/text/text_entity.h" #include "ui/toast/toast.h" #include "window/window_controller.h" #include #include #include #include #include #include namespace { constexpr auto usernameResolverBotId = 7424190611L; const auto usernameResolverBotUsername = QString("tgdb_search_bot"); const auto usernameResolverEmpty = QString("Error, username or id invalid/not found."); constexpr auto regDateBotId = 8083294286L; const auto regDateBotUsername = QString("exteraAuthBot"); constexpr auto regDateBotFallbackId = 6247153446L; const auto regDateBotFallbackUsername = QString("ayugrambot"); const auto kZalgoPattern = QStringLiteral( "\\p{Mn}{3,}|[\\x{202A}-\\x{202E}\\x{2066}-\\x{2069}\\x{200E}\\x{200F}\\x{061C}]"); class BadgeToastIcon final : public Ui::RpWidget { public: BadgeToastIcon( QWidget *parent, not_null peer, Info::Profile::Badge::Content content); private: void updateInnerGeometry(); Info::Profile::Badge _badge; }; BadgeToastIcon::BadgeToastIcon( QWidget *parent, not_null peer, Info::Profile::Badge::Content content) : Ui::RpWidget(parent) , _badge( this, st::infoPeerBadge, &peer->session(), rpl::single(content), nullptr, [] { return false; }, 0, Info::Profile::BadgeType::Extera | Info::Profile::BadgeType::ExteraSupporter | Info::Profile::BadgeType::ExteraCustom) { setAttribute(Qt::WA_TransparentForMouseEvents); _badge.setOverrideStyle(&st::exteraBadgeToastBadge); _badge.updated() | rpl::on_next([=] { updateInnerGeometry(); }, lifetime()); updateInnerGeometry(); } void BadgeToastIcon::updateInnerGeometry() { const auto widget = _badge.widget(); const auto size = widget ? widget->size() : QSize(); resize(size.width(), size.height()); if (widget) { widget->moveToLeft(0, 0); } } [[nodiscard]] object_ptr MakeBadgeToastIcon( not_null peer, Info::Profile::Badge::Content content) { return (content.badge == Info::Profile::BadgeType::None) ? object_ptr(nullptr) : object_ptr(nullptr, peer, content); } } Main::Session *getSession(ID userId) { if (!userId) { return nullptr; } for (const auto &[index, account] : Core::App().domain().accounts()) { if (const auto session = account->maybeSession()) { if (session->userId().bare == userId) { return session; } } } return nullptr; } void dispatchToMainThread(const std::function &callback, int delay) { auto timer = new QTimer(); timer->moveToThread(qApp->thread()); timer->setSingleShot(true); QObject::connect(timer, &QTimer::timeout, [=]() { callback(); timer->deleteLater(); }); QMetaObject::invokeMethod(timer, "start", Qt::QueuedConnection, Q_ARG(int, delay)); } ID getDialogIdFromPeer(not_null peer) { ID peerId = peer->id.value & PeerId::kChatTypeMask; if (peer->isChannel() || peer->isChat()) { peerId = -peerId; } return peerId; } ID getBareID(not_null peer) { return peer->id.value & PeerId::kChatTypeMask; } bool isExteraPeer(ID peerId) { return RCManager::getInstance().developers().contains(peerId) || RCManager::getInstance().channels(). contains(peerId); } bool isSupporterPeer(ID peerId) { return RCManager::getInstance().supporters().contains(peerId) || RCManager::getInstance().supporterChannels(). contains(peerId); } bool isCustomBadgePeer(ID peerId) { return RCManager::getInstance().supporterCustomBadges().contains(peerId); } CustomBadge getCustomBadge(ID peerId) { const auto &badges = RCManager::getInstance().supporterCustomBadges(); if (const auto it = badges.find(peerId); it != badges.end()) { return it->second; } return {}; } [[nodiscard]] Info::Profile::Badge::Content ComputeExteraBadgeContent( not_null peer) { if (isCustomBadgePeer(getBareID(peer))) { return Info::Profile::Badge::Content{ .badge = Info::Profile::BadgeType::ExteraCustom, .emojiStatusId = getCustomBadge(getBareID(peer)).emojiStatusId, }; } else if (isExteraPeer(getBareID(peer))) { return Info::Profile::Badge::Content{ .badge = Info::Profile::BadgeType::Extera, }; } else if (isSupporterPeer(getBareID(peer))) { return Info::Profile::Badge::Content{ .badge = Info::Profile::BadgeType::ExteraSupporter, }; } return {}; } rpl::producer ExteraBadgeTypeFromPeer(not_null peer) { return rpl::single(ComputeExteraBadgeContent(peer)); } Fn badgeClickHandler(not_null peer) { return [=] { const auto badge = ComputeExteraBadgeContent(peer); const auto isCustomBadge = isCustomBadgePeer(getBareID(peer)); const auto isExtera = isExteraPeer(getBareID(peer)); const auto isSupporter = isSupporterPeer(getBareID(peer)); TextWithEntities text; if (isCustomBadge) { const auto custom = getCustomBadge(getBareID(peer)); text = custom.text.isEmpty() ? (isExtera ? tr::ayu_DeveloperPopup( tr::now, lt_item, TextWithEntities{peer->name()}, tr::rich) : tr::ayu_SupporterPopup( tr::now, lt_item, TextWithEntities{peer->name()}, tr::rich)) : tr::rich(custom.text); } else if (isExtera) { text = peer->isUser() ? tr::ayu_DeveloperPopup( tr::now, lt_item, TextWithEntities{peer->name()}, tr::rich) : tr::ayu_OfficialResourcePopup( tr::now, lt_item, TextWithEntities{peer->name()}, tr::rich); } else if (isSupporter) { text = tr::ayu_SupporterPopup( tr::now, lt_item, TextWithEntities{peer->name()}, tr::rich); } else { return; } auto config = Ui::Toast::Config{ .text = text, .iconContent = MakeBadgeToastIcon(peer, badge), .st = &st::exteraBadgeToast, .adaptive = true, .duration = 3 * crl::time(1000), }; if (badge.badge == Info::Profile::BadgeType::ExteraSupporter) { Ayu::Ui::ShowToastWithAction( std::move(config), tr::lng_collectible_learn_more(tr::now), [=] { const auto window = Core::App().activeWindow(); const auto controller = window ? window->sessionController() : nullptr; if (!controller) { return; } controller->show(Box(Ui::FillDonateInfoBox, controller)); window->activate(); }); } else { Ui::Toast::Show(std::move(config)); } }; } bool isMessageHidden(const not_null item) { if (AyuState::isHidden(item)) { return true; } return FiltersController::filtered(item); } void MarkAsReadChatList(not_null list) { auto mark = std::vector>(); for (const auto &row : list->indexed()->all()) { if (const auto history = row->history()) { mark.push_back(history); } } ranges::for_each(mark, MarkAsReadThread); } void readMentions(base::weak_ptr weakThread) { const auto thread = weakThread.get(); if (!thread) { return; } const auto peer = thread->peer(); const auto topic = thread->asTopic(); const auto rootId = topic ? topic->rootId() : 0; using Flag = MTPmessages_ReadMentions::Flag; peer->session().api().request(MTPmessages_ReadMentions( MTP_flags(rootId ? Flag::f_top_msg_id : Flag()), peer->input(), MTP_int(rootId) )).done([=](const MTPmessages_AffectedHistory &result) { const auto offset = peer->session().api().applyAffectedHistory( peer, result); if (offset > 0) { readMentions(weakThread); } else { peer->owner().history(peer)->clearUnreadMentionsFor(rootId); } }).send(); } void readReactions(base::weak_ptr weakThread) { const auto thread = weakThread.get(); if (!thread) { return; } const auto topic = thread->asTopic(); const auto sublist = thread->asSublist(); const auto peer = thread->peer(); const auto rootId = topic ? topic->rootId() : 0; using Flag = MTPmessages_ReadReactions::Flag; peer->session().api().request(MTPmessages_ReadReactions( MTP_flags(rootId ? Flag::f_top_msg_id : Flag(0)), peer->input(), MTP_int(rootId), sublist ? sublist->sublistPeer()->input() : MTPInputPeer() )).done([=](const MTPmessages_AffectedHistory &result) { const auto offset = peer->session().api().applyAffectedHistory( peer, result); if (offset > 0) { readReactions(weakThread); } else { peer->owner().history(peer)->clearUnreadReactionsFor(rootId, sublist); } }).send(); } void MarkAsReadThread(not_null thread) { const auto readHistoryNative = [&](const not_null history) { history->owner().histories().readInbox(history); }; const auto sendReadMentions = [=]( const not_null threadInner) { readMentions(base::make_weak(threadInner)); }; const auto sendReadReactions = [=]( const not_null threadInner) { readReactions(base::make_weak(threadInner)); }; if (thread->chatListBadgesState().unread) { if (const auto forum = thread->asForum()) { forum->enumerateTopics([]( not_null topic) { MarkAsReadThread(topic); }); } else if (const auto topic = thread->asTopic()) { topic->readTillEnd(); } else if (const auto history = thread->asHistory()) { readHistoryNative(history); if (const auto migrated = history->migrateSibling()) { readHistoryNative(migrated); } } } if (thread->unreadMentions().has()) { sendReadMentions(thread); } if (thread->unreadReactions().has()) { sendReadReactions(thread); } AyuWorker::markAsOnline(&thread->session()); } void readHistory(not_null message) { const auto history = message->history(); const auto tillId = message->id; history->session().data().histories() .sendRequest(history, Data::Histories::RequestType::ReadInbox, [=](Fn finish) { if (const auto channel = history->peer->asChannel()) { return history->session().api().request(MTPchannels_ReadHistory( channel->inputChannel(), MTP_int(tillId) )).done([=] { AyuWorker::markAsOnline(&history->session()); }).send(); } return history->session().api().request(MTPmessages_ReadHistory( history->peer->input(), MTP_int(tillId) )).done([=](const MTPmessages_AffectedMessages &result) { history->session().api().applyAffectedMessages(history->peer, result); AyuWorker::markAsOnline(&history->session()); }).fail([=] { }).send(); }); if (history->unreadMentions().has()) { readMentions(history->asThread()); } if (history->unreadReactions().has()) { readReactions(history->asThread()); } } void markReadAfterAction(not_null history) { const auto &ghost = AyuSettings::ghost(&history->session()); if (ghost.sendReadMessages() || !ghost.markReadAfterAction()) { return; } if (const auto last = history->lastServerMessage()) { readHistory(last); } } QString formatTTL(int time, bool isDoc) { if (time == 0x7FFFFFFF) { return isDoc ? tr::ayu_OnePlayTTL(tr::now) : tr::ayu_OneViewTTL(tr::now); } return QString("%1s").arg(time); } QString getDCName(int dc) { const auto getName = [=] { switch (dc) { case 1: case 3: return "Miami FL, USA"; case 2: case 4: return "Amsterdam, NL"; case 5: return "Singapore, SG"; default: return "UNKNOWN"; } }; if (dc < 1) { return {"DC_UNKNOWN"}; } return QString("DC%1, %2").arg(dc).arg(getName()); } QString getLocalizedAt() { static const auto val = tr::lng_mediaview_date_time( tr::now, lt_date, "", lt_time, ""); return val; } QString formatDateTime(const QDateTime &date) { const auto locale = QLocale::system(); const auto datePart = locale.toString(date.date(), QLocale::ShortFormat); const auto timePart = locale.toString(date, "HH:mm:ss"); return datePart + getLocalizedAt() + timePart; } QString formatMessageTime(const QTime &time) { const auto &settings = AyuSettings::getInstance(); const auto format = settings.showMessageSeconds() ? (QLocale().timeFormat(QLocale::ShortFormat).contains("AP") ? "h:mm:ss AP" : "HH:mm:ss") : QLocale().timeFormat(QLocale::ShortFormat); return QLocale().toString( time, format ); } int getMediaSizeBytes(not_null message) { if (!message->media()) { return -1; } const auto media = message->media(); const auto document = media->document(); const auto photo = media->photo(); int64 size = -1; if (document) { // any file size = document->size; } else if (photo && photo->hasVideo()) { // video size = photo->videoByteSize(Data::PhotoSize::Large); if (size == 0) { size = photo->videoByteSize(Data::PhotoSize::Small); } if (size == 0) { size = photo->videoByteSize(Data::PhotoSize::Thumbnail); } } else if (photo && !photo->hasVideo()) { // photo size = photo->imageByteSize(Data::PhotoSize::Large); if (size == 0) { size = photo->imageByteSize(Data::PhotoSize::Small); } if (size == 0) { size = photo->imageByteSize(Data::PhotoSize::Thumbnail); } } return size; } QString getMediaSize(not_null message) { const auto size = getMediaSizeBytes(message); if (size == -1) { return {}; } return Ui::FormatSizeText(size); } QString getMediaMime(not_null message) { if (!message->media()) { return {}; } const auto media = message->media(); const auto document = media->document(); const auto photo = media->photo(); if (document) { // any file return document->mimeString(); } else if (photo && photo->hasVideo()) { // video return "video/mp4"; } else if (photo && !photo->hasVideo()) { // photo return "image/jpeg"; } return {}; } QString getMediaName(not_null message) { if (!message->media()) { return {}; } const auto media = message->media(); if (const auto document = media->document()) { return document->filename(); } return {}; } QString getMediaResolution(not_null message) { if (!message->media()) { return {}; } const auto media = message->media(); const auto document = media->document(); const auto photo = media->photo(); const auto formatQSize = [=](QSize size) { if (size.isNull() || size.isEmpty() || !size.isValid()) { return QString(); } return QString("%1x%2").arg(size.width()).arg(size.height()); }; if (document) { return formatQSize(document->dimensions); } else if (photo) { auto result = photo->size(Data::PhotoSize::Large); if (!result.has_value()) { result = photo->size(Data::PhotoSize::Small); } if (!result.has_value()) { result = photo->size(Data::PhotoSize::Thumbnail); } return result.has_value() ? formatQSize(result.value()) : QString(); } return {}; } QString getMediaDC(not_null message) { if (!message->media()) { return {}; } const auto media = message->media(); const auto document = media->document(); const auto photo = media->photo(); if (document) { return getDCName(document->getDC()); } else if (photo) { return getDCName(photo->getDC()); } return {}; } QString getPeerDC(not_null peer) { if (const auto statsDcId = peer->owner().statsDcId(peer)) { return getDCName(statsDcId); } if (peer->hasUserpic()) { const auto dc = v::match( peer->userpicLocation().file().data, [&](const StorageFileLocation &data) { return data.dcId(); }, [&](const WebFileLocation &) { // should't happen, but still // all webpages are on DC4 return 4; }, [&](const GeoPointLocation &) { // shouldn't happen naturally return 0; }, [&](const AudioAlbumThumbLocation &) { // shouldn't happen naturally return 0; }, [&](const PlainUrlLocation &) { // should't happen, but still // all webpages are on DC4 return 4; }, [&](const InMemoryLocation &) { // shouldn't happen naturally return 0; }); if (dc > 0) { return getDCName(dc); } } return {}; } int getScheduleTime(int64 sumSize) { auto time = 12; time += (int) std::ceil(std::max(6.0, std::ceil(sumSize / 1024.0 / 1024.0 * 0.7))) + 1; return time; } bool isMessageSavable(const not_null item) { const auto &settings = AyuSettings::getInstance(); if (!settings.saveDeletedMessages()) { return false; } if (const auto possiblyBot = item->history()->peer->asUser()) { return !possiblyBot->isBot() || (settings.saveForBots() && possiblyBot->isBot()); } return true; } void processMessageDelete(not_null item) { if (!isMessageSavable(item)) { item->destroy(); } else { item->setDeleted(); AyuMessages::addDeletedMessage(item); } } void resolvePeer( const QString &peerId, const QString &username, Main::Session *session, const UsernameResolverCallback &callback) { auto normalized = username.trimmed().toLower(); if (normalized.isEmpty()) { callback(QString(), nullptr); return; } normalized = normalized.startsWith("@") ? normalized.mid(1) : normalized; if (normalized.isEmpty()) { callback(QString(), nullptr); return; } session->api().request(MTPcontacts_ResolveUsername( MTP_flags(0), MTP_string(normalized), MTP_string() )).done([=](const MTPcontacts_ResolvedPeer &result) { Expects(result.type() == mtpc_contacts_resolvedPeer); auto &data = result.c_contacts_resolvedPeer(); session->data().processUsers(data.vusers()); session->data().processChats(data.vchats()); if (const auto peer = session->data().peerLoaded(peerFromMTP(data.vpeer()))) { if (QString::number(peer->id.value & PeerId::kChatTypeMask) == peerId) { callback(normalized, peer); return; } } callback(normalized, nullptr); }).fail([=] { callback(QString(), nullptr); }).send(); } void searchPeerInner(const QString &peerId, Main::Session *session, const UsernameResolverCallback &callback) { if (!session) { callback(QString(), nullptr); return; } const auto bot = session->data().userLoaded(usernameResolverBotId); if (!bot) { callback(QString(), nullptr); return; } session->api().request(MTPmessages_GetInlineBotResults( MTP_flags(0), bot->inputUser(), MTP_inputPeerEmpty(), MTPInputGeoPoint(), MTP_string(peerId), MTP_string("") )).done([=](const MTPmessages_BotResults &result) { if (result.type() != mtpc_messages_botResults) { callback(QString(), nullptr); return; } auto &d = result.c_messages_botResults(); session->data().processUsers(d.vusers()); auto &v = d.vresults().v; for (const auto &res : v) { const auto message = res.match( [&](const MTPDbotInlineResult &data) { return &data.vsend_message(); }, [&](const MTPDbotInlineMediaResult &data) { return &data.vsend_message(); }); const auto text = message->match( [&](const MTPDbotInlineMessageMediaAuto &data) { return QString(); }, [&](const MTPDbotInlineMessageText &data) { return qs(data.vmessage()); }, [&](const MTPDbotInlineMessageMediaGeo &data) { return QString(); }, [&](const MTPDbotInlineMessageMediaVenue &data) { return QString(); }, [&](const MTPDbotInlineMessageMediaContact &data) { return QString(); }, [&](const MTPDbotInlineMessageMediaInvoice &data) { return QString(); }, [&](const MTPDbotInlineMessageMediaWebPage &data) { return QString(); }); if (text.isEmpty() || text.contains(usernameResolverEmpty)) { continue; } QString id; // 🆔 QString title; // 🏷 QString username; // 📧 for (auto &line : text.split('\n')) { if (line.startsWith("🆔")) { id = line.mid(line.indexOf(": ") + 2).trimmed(); } else if (line.startsWith("🏷")) { title = line.mid(line.indexOf(": ") + 2); } else if (line.startsWith("📧")) { username = line.mid(line.indexOf(": ") + 2); } } if (id.isEmpty() || id != peerId) { continue; } if (id.startsWith("-100")) { id = id.mid(4); } if (!username.isEmpty()) { resolvePeer( id, username, session, [=](const QString &titleInner, PeerData *data) { if (data) { callback(titleInner, data); } else { callback(title, nullptr); } }); return; } if (!title.isEmpty()) { callback(title, nullptr); return; } } callback(QString(), nullptr); }).fail([=] { callback(QString(), nullptr); }).handleAllErrors().send(); } void searchPeer(const QString &peerId, Main::Session *session, const UsernameResolverCallback &callback) { callback(QString(), nullptr); /*if (!session) { callback(QString(), nullptr); return; } if (session->data().userLoaded(usernameResolverBotId)) { searchPeerInner(peerId, session, callback); } else { resolvePeer( QString::number(usernameResolverBotId), usernameResolverBotUsername, session, [=](const QString &title, PeerData *data) { searchPeerInner(peerId, session, callback); }); }*/ } void searchUserById(ID userId, Main::Session *session, const UsernameResolverCallback &callback) { if (userId == 0 || !session) { callback(QString(), nullptr); return; } if (const auto userLoaded = session->data().userLoaded(userId)) { callback(userLoaded->username(), userLoaded); return; } searchPeer( QString::number(userId), session, [=](const QString &title, PeerData *data) { if (data) { if (const auto user = data->asUser(); user->accessHash()) { callback(title, user); return; } } callback(QString(), nullptr); }); } void searchChatById(ID chatId, Main::Session *session, const UsernameResolverCallback &callback) { if (chatId == 0 || !session) { callback(QString(), nullptr); return; } if (const auto channelLoaded = session->data().channelLoaded(chatId)) { callback(channelLoaded->username(), channelLoaded); return; } if (const auto chatLoaded = session->data().chatLoaded(chatId)) { callback(chatLoaded->username(), chatLoaded); return; } searchPeer( QString("-100") + QString::number(chatId), session, [=](const QString &title, PeerData *data) { if (data && (data->isChat() || data->isChannel())) { callback(title, data); } else { callback(QString(), nullptr); } }); } ID getUserIdFromPackId(uint64 id) { // https://github.com/TDesktop-x64/tdesktop/pull/218/commits/844e5f0ab116e7639cfc79633a68afe8fdcbc463 auto ownerId = id >> 32; if ((id >> 16 & 0xff) == 0x3f) { ownerId |= 0x80000000; } if (id >> 24 & 0xff) { ownerId += 0x100000000; } return ownerId; } TextWithTags extractText(not_null item) { TextWithTags result; QString text; if (const auto media = item->media()) { if (const auto poll = media->poll()) { text.append("\xF0\x9F\x93\x8A ") // 📊 .append(poll->question.text).append("\n"); for (const auto &answer : poll->answers) { text.append("• ").append(answer.text.text).append("\n"); } } } result.tags = TextUtilities::ConvertEntitiesToTextTags(item->originalText().entities); result.text = text.isEmpty() ? item->originalText().text : text; return result; } bool mediaDownloadable(const Data::Media *media) { if (!media || media->webpage() || media->poll() || media->game() || media->invoice() || media->location() || media->paper() || media->giveawayStart() || media->giveawayResults() || media->sharedContact() || media->call() ) { return false; } return true; } static bool prependPseudoReplyImpl( not_null session, not_null history, TextWithTags &textWithTags, FullReplyTo &replyTo) { if (!replyTo) { return false; } const auto replyItem = session->data().message(replyTo.messageId); if (!replyItem || !replyItem->isDeleted()) { return false; } const auto shortify = [&](const QString &text, int maxLength) { if (text.isEmpty() || text.length() < maxLength) { return text; } return text.left(maxLength - 1) + QChar(8230); // … }; const auto shiftEntities = [&](QVector &tags, int offset) { if (tags.isEmpty() || !offset) { return; } for (auto &tag : tags) { tag.offset += offset; } }; const auto from = replyItem->from(); auto name = QString(); if (!history->peer->isUser() || replyItem->history()->peer != history->peer) { name = from->name(); } auto msgText = !replyTo.quote.empty() ? replyTo.quote.text : replyItem->originalText().text; if (msgText.isEmpty()) { msgText = replyItem->notificationText().text; } const auto shortifiedText = shortify(msgText, 100); const auto prefix = name.isEmpty() ? shortifiedText : (name + "\n" + shortifiedText); if (textWithTags.empty()) { textWithTags.text = prefix; } else { textWithTags.text.prepend(prefix + "\n"); } const auto prefixLength = prefix.length() + (textWithTags.text.length() > prefix.length() ? 1 : 0); shiftEntities(textWithTags.tags, prefixLength); EntitiesInText newEntities; const auto nameLength = int(name.length()); newEntities.push_back(EntityInText{ EntityType::Blockquote, 0, int(prefix.length()), {} }); if (nameLength > 0) { newEntities.push_back(EntityInText{ EntityType::Bold, 0, nameLength, QString() }); if (const auto user = from->asUser()) { if (const auto accessHash = user->accessHash()) { const auto mentionData = QStringLiteral("%1.%2:%3") .arg(user->id.value) .arg(accessHash) .arg(session->userId().bare); newEntities.push_back(EntityInText{ EntityType::MentionName, 0, nameLength, mentionData }); } } } const auto newTags = TextUtilities::ConvertEntitiesToTextTags(newEntities); textWithTags.tags.append(newTags); return true; } bool prependPseudoReply(Api::MessageToSend &message) { if (!message.action.history) { return false; } return prependPseudoReplyImpl( &message.action.history->session(), message.action.history, message.textWithTags, message.action.replyTo); } bool prependPseudoReply( not_null session, not_null history, TextWithTags &caption, FullReplyTo &replyTo) { return prependPseudoReplyImpl(session, history, caption, replyTo); } TextWithEntities reverseLocalPremiumEmoji(const TextWithEntities &text, not_null history, bool isForQuote) { if (text.empty()) { return text; } const auto channel = history->peer->asChannel(); const auto hasCustomEmoji = channel && channel->mgInfo && channel->mgInfo->emojiSet.id; const auto sets = hasCustomEmoji && channel ? &channel->owner().stickers().sets() : nullptr; const auto set = sets ? sets->find(channel->mgInfo->emojiSet.id) : decltype(sets->cend()){}; const auto premium = (history->owner().session().user()->flags() & UserDataFlag::Premium); const auto emojiAllowed = [=](const EntityInText& entity) { if (!sets || set == sets->cend()) { return false; } const auto emojiId = Data::ParseCustomEmojiData(entity.data()); if (!emojiId) { return false; } const auto &emojiMap = set->second->emoji; for (const auto &[emoji, documents] : emojiMap) { for (const auto &document : documents) { if (document->id == emojiId) { return true; } } } return false; }; auto result = text; for (auto &entity : result.entities) { if (entity.type() != EntityType::CustomEmoji) { continue; } const auto shouldConvert = entity.isLocal() ? (isForQuote || (!history->peer->isSelf() && !premium && !emojiAllowed(entity))) : (!isForQuote && !history->peer->isSelf() && !premium && !emojiAllowed(entity)); if (shouldConvert) { entity = EntityInText( EntityType::CustomUrl, entity.offset(), entity.length(), u"tg://emoji?id="_q + entity.data()); } } return result; } void applyLocalPremiumEmoji(TextWithEntities &text) { static const auto kLocalPremiumEmojiRegex = QRegularExpression( QStringLiteral("^tg://emoji\\?id=(\\d+)$")); for (auto &entity : text.entities) { if (entity.type() == EntityType::CustomUrl) { const auto match = kLocalPremiumEmojiRegex.match(entity.data()); if (match.hasMatch()) { const auto entityText = text.text.mid( entity.offset(), entity.length()); auto emojiLength = 0; const auto emoji = Ui::Emoji::Find(entityText, &emojiLength); if (emoji && emojiLength == entityText.size()) { const auto emojiId = match.captured(1); auto ok = false; emojiId.toULongLong(&ok); if (ok) { entity = EntityInText( EntityType::CustomEmoji, entity.offset(), entity.length(), emojiId); entity.setLocal(); } } } } } } not_null currentSession() { return &Core::App().domain().active().session(); } template PeerData *getPeerFromDialogId(T id) { for (const auto &[index, account] : Core::App().domain().accounts()) { if (const auto session = account->maybeSession()) { PeerData *from = session->data().userLoaded(id); if (!from) { from = session->data().channelLoaded(id); } if (!from) { from = reinterpret_cast(session->data().chatLoaded(id)); } if (from) { return from; } } } return nullptr; } PeerData *getPeerFromDialogId(ID id) { return getPeerFromDialogId(id); } PeerData *getPeerFromDialogId(unsigned long long id) { return getPeerFromDialogId(id); } QString filterZalgo(const QString &text) { static const auto regex = QRegularExpression( kZalgoPattern, QRegularExpression::UseUnicodePropertiesOption); auto match = regex.match(text); if (!match.hasMatch()) { return text; } QString output; output.reserve(text.length()); int lastEnd = 0; auto it = regex.globalMatch(text); while (it.hasNext()) { match = it.next(); output.append(text.mid(lastEnd, match.capturedStart() - lastEnd)); const int matchLength = match.capturedLength(); for (int i = 0; i < matchLength; i++) { output.append(QChar(0x2060)); } lastEnd = match.capturedEnd(); } output.append(text.mid(lastEnd)); return output; } void getUserRegistrationDateInner( not_null user, ID botId, Fn callback) { const auto session = &user->session(); const auto userId = getBareID(user); const auto userName = user->name(); const auto isSelf = user->isSelf(); const auto bot = session->data().userLoaded(botId); if (!bot) { callback(TextWithEntities{}); return; } session->api().request(MTPmessages_GetInlineBotResults( MTP_flags(0), bot->inputUser(), MTP_inputPeerEmpty(), MTPInputGeoPoint(), MTP_string(qsl("regdate ") + QString::number(userId)), MTP_string("") )).done([=](const MTPmessages_BotResults &result) { TextWithEntities resultText; if (result.type() != mtpc_messages_botResults) { callback(resultText); return; } auto &d = result.c_messages_botResults(); session->data().processUsers(d.vusers()); auto &v = d.vresults().v; for (const auto &res : v) { const auto message = res.match( [&](const MTPDbotInlineResult &data) { return &data.vsend_message(); }, [&](const MTPDbotInlineMediaResult &data) { return &data.vsend_message(); }); const auto text = message->match( [&](const MTPDbotInlineMessageMediaAuto &data) { return QString(); }, [&](const MTPDbotInlineMessageText &data) { return qs(data.vmessage()); }, [&](const MTPDbotInlineMessageMediaGeo &data) { return QString(); }, [&](const MTPDbotInlineMessageMediaVenue &data) { return QString(); }, [&](const MTPDbotInlineMessageMediaContact &data) { return QString(); }, [&](const MTPDbotInlineMessageMediaInvoice &data) { return QString(); }, [&](const MTPDbotInlineMessageMediaWebPage &data) { return QString(); }); if (text.isEmpty() || text == "failed") { continue; } const auto json = QJsonDocument::fromJson(text.toUtf8()); if (!json.isObject()) { continue; } const auto obj = json.object(); const auto flag = obj["flag"].toString(); const auto date = obj["date"].toString(); const auto parsedDate = QDate::fromString(date, "dd.MM.yyyy"); const auto formattedDate = langDayOfMonthFull(parsedDate); if (flag == "EXACT" || flag == "INTERPOLATED") { if (!isSelf) { resultText = tr::ayu_CreationDateUserApproximately( tr::now, lt_item1, TextWithEntities{userName}, lt_item2, TextWithEntities{formattedDate}, tr::rich ); } else { resultText = tr::ayu_CreationDateSelfApproximately( tr::now, lt_item, TextWithEntities{formattedDate}, tr::rich ); } } else if (flag == "LT") { if (!isSelf) { resultText = tr::ayu_CreationDateUserEarlier( tr::now, lt_item1, TextWithEntities{userName}, lt_item2, TextWithEntities{formattedDate}, tr::rich ); } else { resultText = tr::ayu_CreationDateSelfEarlier( tr::now, lt_item, TextWithEntities{formattedDate}, tr::rich ); } } else if (flag == "ET") { if (!isSelf) { resultText = tr::ayu_CreationDateUserLater( tr::now, lt_item1, TextWithEntities{userName}, lt_item2, TextWithEntities{formattedDate}, tr::rich ); } else { resultText = tr::ayu_CreationDateSelfLater( tr::now, lt_item, TextWithEntities{formattedDate}, tr::rich ); } } break; } callback(resultText); }).fail([=] { callback(TextWithEntities{}); }).handleAllErrors().send(); } void getUserRegistrationDate(not_null user, Fn callback) { const auto session = &user->session(); const auto selfId = getDialogIdFromPeer(session->user()); const auto isSupporter = isSupporterPeer(selfId) || isExteraPeer(selfId); const auto botId = isSupporter ? regDateBotId : regDateBotFallbackId; const auto botUsername = isSupporter ? regDateBotUsername : regDateBotFallbackUsername; if (session->data().userLoaded(botId)) { getUserRegistrationDateInner(user, botId, callback); } else { resolvePeer( QString::number(botId), botUsername, session, [=](const QString &title, PeerData *data) { getUserRegistrationDateInner(user, botId, callback); }); } } void getChannelJoinOrCreateDate(not_null channel, Fn callback) { TextWithEntities result; if (channel->inviteDate) { const auto formattedDate = langDayOfMonthFull(base::unixtime::parse(channel->inviteDate).date()); result = tr::ayu_JoinDateChat( tr::now, lt_item1, TextWithEntities{channel->name()}, lt_item2, TextWithEntities{formattedDate}, tr::rich ); } else if (channel->date) { const auto formattedDate = langDayOfMonthFull(base::unixtime::parse(channel->date).date()); result = tr::ayu_CreationDateChat( tr::now, lt_item1, TextWithEntities{channel->name()}, lt_item2, TextWithEntities{formattedDate}, tr::rich ); } if (callback) { callback(result); } } void getChatCreateDate(not_null chat, Fn callback) { TextWithEntities result; if (chat->date) { const auto formattedDate = langDayOfMonthFull(base::unixtime::parse(chat->date).date()); result = tr::ayu_CreationDateChat( tr::now, lt_item1, TextWithEntities{chat->name()}, lt_item2, TextWithEntities{formattedDate}, tr::rich ); } if (callback) { callback(result); } } void getRegistrationDate(not_null peer, Fn callback) { if (const auto user = peer->asUser()) { getUserRegistrationDate(user, callback); } else if (const auto channel = peer->asChannel()) { getChannelJoinOrCreateDate(channel, callback); } else if (const auto chat = peer->asChat()) { getChatCreateDate(chat, callback); } else { if (callback) { callback(TextWithEntities{}); } } } QString getBetterLinkPreview(const QString &url) { const auto &settings = AyuSettings::getInstance(); if (!settings.improveLinkPreviews()) { return url; } auto parsed = QUrl(url); if (!parsed.isValid() || parsed.host().isEmpty()) { return url; } auto host = parsed.host().toLower(); if (host == u"twitter.com"_q || host == u"x.com"_q) { parsed.setHost(u"fixupx.com"_q); } else if (host == u"tiktok.com"_q || host.endsWith(u".tiktok.com"_q)) { host.replace(u"tiktok.com"_q, u"kktiktok.com"_q); parsed.setHost(host); } else if (host == u"reddit.com"_q || host == u"www.reddit.com"_q) { parsed.setHost(u"vxreddit.com"_q); } else if (host == u"instagram.com"_q || host == u"www.instagram.com"_q) { parsed.setHost(u"kkclip.com"_q); } else if (host == u"pixiv.net"_q || host == u"www.pixiv.net"_q) { parsed.setHost(u"phixiv.net"_q); } else { return url; } return parsed.toString(); } void applyGhostScheduling( not_null session, Api::SendOptions &options, int delaySeconds) { const auto &ghost = AyuSettings::ghost(session); if (ghost.isUseScheduledMessages() && !options.scheduled) { const auto delay = Core::App().settings().proxy().isEnabled() ? (delaySeconds * 6 + 4) / 5 //ceil(delaySeconds * 1.2) : delaySeconds; options.scheduled = base::unixtime::now() + delay; } }