// 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, 2024 #include "ayu/ui/sections/edited/edited_log_item.h" #include "ayu/data/entities.h" #include "api/api_chat_participants.h" #include "api/api_text_entities.h" #include "ayu/ui/sections/edited/edited_log_inner.h" #include "base/unixtime.h" #include "core/application.h" #include "core/click_handler_types.h" #include "data/data_channel.h" #include "data/data_file_origin.h" #include "data/data_forum_topic.h" #include "data/data_session.h" #include "data/data_user.h" #include "history/history.h" #include "history/history_item.h" #include "history/history_item_helpers.h" #include "history/view/history_view_element.h" #include "lang/lang_keys.h" #include "main/main_session.h" #include "ui/basic_click_handlers.h" #include "ui/text/text_utilities.h" namespace EditedLog { namespace { const auto CollectChanges = []( auto &phraseMap, auto plusFlags, auto minusFlags) { auto withPrefix = [&phraseMap](auto flags, QChar prefix) { auto result = QString(); for (auto &phrase : phraseMap) { if (flags & phrase.first) { result.append('\n' + (prefix + phrase.second(tr::now))); } } return result; }; const auto kMinus = QChar(0x2212); return withPrefix(plusFlags & ~minusFlags, '+') + withPrefix(minusFlags & ~plusFlags, kMinus); }; TextWithEntities GenerateAdminChangeText( not_null channel, const TextWithEntities &user, ChatAdminRightsInfo newRights, ChatAdminRightsInfo prevRights) { using Flag = ChatAdminRight; using Flags = ChatAdminRights; auto result = tr::lng_admin_log_promoted( tr::now, lt_user, user, Ui::Text::WithEntities); const auto useInviteLinkPhrase = channel->isMegagroup() && channel->anyoneCanAddMembers(); const auto invitePhrase = useInviteLinkPhrase ? tr::lng_admin_log_admin_invite_link : tr::lng_admin_log_admin_invite_users; const auto callPhrase = channel->isBroadcast() ? tr::lng_admin_log_admin_manage_calls_channel : tr::lng_admin_log_admin_manage_calls; static auto phraseMap = std::map>{ {Flag::ChangeInfo, tr::lng_admin_log_admin_change_info}, {Flag::PostMessages, tr::lng_admin_log_admin_post_messages}, {Flag::EditMessages, tr::lng_admin_log_admin_edit_messages}, {Flag::DeleteMessages, tr::lng_admin_log_admin_delete_messages}, {Flag::BanUsers, tr::lng_admin_log_admin_ban_users}, {Flag::InviteByLinkOrAdd, invitePhrase}, {Flag::ManageTopics, tr::lng_admin_log_admin_manage_topics}, {Flag::PinMessages, tr::lng_admin_log_admin_pin_messages}, {Flag::ManageCall, tr::lng_admin_log_admin_manage_calls}, {Flag::AddAdmins, tr::lng_admin_log_admin_add_admins}, {Flag::Anonymous, tr::lng_admin_log_admin_remain_anonymous}, }; phraseMap[Flag::InviteByLinkOrAdd] = invitePhrase; phraseMap[Flag::ManageCall] = callPhrase; if (!channel->isMegagroup()) { // Don't display "Ban users" changes in channels. newRights.flags &= ~Flag::BanUsers; prevRights.flags &= ~Flag::BanUsers; } const auto changes = CollectChanges( phraseMap, newRights.flags, prevRights.flags); if (!changes.isEmpty()) { result.text.append('\n' + changes); } return result; }; QString GeneratePermissionsChangeText( ChatRestrictionsInfo newRights, ChatRestrictionsInfo prevRights) { using Flag = ChatRestriction; using Flags = ChatRestrictions; static auto phraseMap = std::map>{ {Flag::ViewMessages, tr::lng_admin_log_banned_view_messages}, {Flag::SendOther, tr::lng_admin_log_banned_send_messages}, {Flag::SendPhotos, tr::lng_admin_log_banned_send_photos}, {Flag::SendVideos, tr::lng_admin_log_banned_send_videos}, {Flag::SendMusic, tr::lng_admin_log_banned_send_music}, {Flag::SendFiles, tr::lng_admin_log_banned_send_files}, { Flag::SendVoiceMessages, tr::lng_admin_log_banned_send_voice_messages }, { Flag::SendVideoMessages, tr::lng_admin_log_banned_send_video_messages }, { Flag::SendStickers | Flag::SendGifs | Flag::SendInline | Flag::SendGames, tr::lng_admin_log_banned_send_stickers }, {Flag::EmbedLinks, tr::lng_admin_log_banned_embed_links}, {Flag::SendPolls, tr::lng_admin_log_banned_send_polls}, {Flag::ChangeInfo, tr::lng_admin_log_admin_change_info}, {Flag::AddParticipants, tr::lng_admin_log_admin_invite_users}, {Flag::CreateTopics, tr::lng_admin_log_admin_create_topics}, {Flag::PinMessages, tr::lng_admin_log_admin_pin_messages}, }; return CollectChanges(phraseMap, prevRights.flags, newRights.flags); } TextWithEntities GeneratePermissionsChangeText( PeerId participantId, const TextWithEntities &user, ChatRestrictionsInfo newRights, ChatRestrictionsInfo prevRights) { using Flag = ChatRestriction; const auto newFlags = newRights.flags; const auto newUntil = newRights.until; const auto prevFlags = prevRights.flags; const auto indefinitely = ChannelData::IsRestrictedForever(newUntil); if (newFlags & Flag::ViewMessages) { return tr::lng_admin_log_banned( tr::now, lt_user, user, Ui::Text::WithEntities); } else if (newFlags == 0 && (prevFlags & Flag::ViewMessages) && !peerIsUser(participantId)) { return tr::lng_admin_log_unbanned( tr::now, lt_user, user, Ui::Text::WithEntities); } const auto untilText = indefinitely ? tr::lng_admin_log_restricted_forever(tr::now) : tr::lng_admin_log_restricted_until( tr::now, lt_date, langDateTime(base::unixtime::parse(newUntil))); auto result = tr::lng_admin_log_restricted( tr::now, lt_user, user, lt_until, TextWithEntities{untilText}, Ui::Text::WithEntities); const auto changes = GeneratePermissionsChangeText(newRights, prevRights); if (!changes.isEmpty()) { result.text.append('\n' + changes); } return result; } QString PublicJoinLink() { return u"(public_join_link)"_q; } QString ExtractInviteLink(const MTPExportedChatInvite &data) { return data.match([&](const MTPDchatInviteExported &data) { return qs(data.vlink()); }, [&](const MTPDchatInvitePublicJoinRequests &data) { return PublicJoinLink(); }); } QString ExtractInviteLinkLabel(const MTPExportedChatInvite &data) { return data.match([&](const MTPDchatInviteExported &data) { return qs(data.vtitle().value_or_empty()); }, [&](const MTPDchatInvitePublicJoinRequests &data) { return PublicJoinLink(); }); } QString InternalInviteLinkUrl(const MTPExportedChatInvite &data) { const auto base64 = ExtractInviteLink(data).toUtf8().toBase64(); return "internal:show_invite_link/?link=" + QString::fromLatin1(base64); } QString GenerateInviteLinkText(const MTPExportedChatInvite &data) { const auto label = ExtractInviteLinkLabel(data); return label.isEmpty() ? ExtractInviteLink(data).replace( u"https://"_q, QString() ).replace( u"t.me/joinchat/"_q, QString() ) : label; } TextWithEntities GenerateInviteLinkLink(const MTPExportedChatInvite &data) { const auto text = GenerateInviteLinkText(data); return text.endsWith(Ui::kQEllipsis) ? TextWithEntities{.text = text} : Ui::Text::Link(text, InternalInviteLinkUrl(data)); } TextWithEntities GenerateInviteLinkChangeText( const MTPExportedChatInvite &newLink, const MTPExportedChatInvite &prevLink) { auto link = TextWithEntities{GenerateInviteLinkText(newLink)}; if (!link.text.endsWith(Ui::kQEllipsis)) { link.entities.push_back({ EntityType::CustomUrl, 0, int(link.text.size()), InternalInviteLinkUrl(newLink) }); } auto result = tr::lng_admin_log_edited_invite_link( tr::now, lt_link, link, Ui::Text::WithEntities); result.text.append('\n'); const auto label = [](const MTPExportedChatInvite &link) { return link.match([](const MTPDchatInviteExported &data) { return qs(data.vtitle().value_or_empty()); }, [&](const MTPDchatInvitePublicJoinRequests &data) { return PublicJoinLink(); }); }; const auto expireDate = [](const MTPExportedChatInvite &link) { return link.match([](const MTPDchatInviteExported &data) { return data.vexpire_date().value_or_empty(); }, [&](const MTPDchatInvitePublicJoinRequests &data) { return TimeId(); }); }; const auto usageLimit = [](const MTPExportedChatInvite &link) { return link.match([](const MTPDchatInviteExported &data) { return data.vusage_limit().value_or_empty(); }, [&](const MTPDchatInvitePublicJoinRequests &data) { return 0; }); }; const auto requestApproval = [](const MTPExportedChatInvite &link) { return link.match([](const MTPDchatInviteExported &data) { return data.is_request_needed(); }, [&](const MTPDchatInvitePublicJoinRequests &data) { return true; }); }; const auto wrapDate = [](TimeId date) { return date ? langDateTime(base::unixtime::parse(date)) : tr::lng_group_invite_expire_never(tr::now); }; const auto wrapUsage = [](int count) { return count ? QString::number(count) : tr::lng_group_invite_usage_any(tr::now); }; const auto wasLabel = label(prevLink); const auto nowLabel = label(newLink); const auto wasExpireDate = expireDate(prevLink); const auto nowExpireDate = expireDate(newLink); const auto wasUsageLimit = usageLimit(prevLink); const auto nowUsageLimit = usageLimit(newLink); const auto wasRequestApproval = requestApproval(prevLink); const auto nowRequestApproval = requestApproval(newLink); if (wasLabel != nowLabel) { result.text.append('\n').append( tr::lng_admin_log_invite_link_label( tr::now, lt_previous, wasLabel, lt_limit, nowLabel)); } if (wasExpireDate != nowExpireDate) { result.text.append('\n').append( tr::lng_admin_log_invite_link_expire_date( tr::now, lt_previous, wrapDate(wasExpireDate), lt_limit, wrapDate(nowExpireDate))); } if (wasUsageLimit != nowUsageLimit) { result.text.append('\n').append( tr::lng_admin_log_invite_link_usage_limit( tr::now, lt_previous, wrapUsage(wasUsageLimit), lt_limit, wrapUsage(nowUsageLimit))); } if (wasRequestApproval != nowRequestApproval) { result.text.append('\n').append( nowRequestApproval ? tr::lng_admin_log_invite_link_request_needed(tr::now) : tr::lng_admin_log_invite_link_request_not_needed(tr::now)); } result.entities.push_front( EntityInText(EntityType::Italic, 0, result.text.size())); return result; }; auto GenerateParticipantString( not_null session, PeerId participantId) { // User name in "User name (@username)" format with entities. const auto peer = session->data().peer(participantId); auto name = TextWithEntities{peer->name()}; if (const auto user = peer->asUser()) { const auto data = TextUtilities::MentionNameDataFromFields({ .selfId = session->userId().bare, .userId = peerToUser(user->id).bare, .accessHash = user->accessHash(), }); name.entities.push_back({ EntityType::MentionName, 0, int(name.text.size()), data, }); } const auto username = peer->username(); if (username.isEmpty()) { return name; } auto mention = TextWithEntities{'@' + username}; mention.entities.push_back({ EntityType::Mention, 0, int(mention.text.size()) }); return tr::lng_admin_log_user_with_username( tr::now, lt_name, name, lt_mention, mention, Ui::Text::WithEntities); } auto GenerateParticipantChangeText( not_null channel, const Api::ChatParticipant &participant, std::optional oldParticipant = std::nullopt) { using Type = Api::ChatParticipant::Type; const auto oldRights = oldParticipant ? oldParticipant->rights() : ChatAdminRightsInfo(); const auto oldRestrictions = oldParticipant ? oldParticipant->restrictions() : ChatRestrictionsInfo(); const auto generateOther = [&](PeerId participantId) { auto user = GenerateParticipantString( &channel->session(), participantId); if (oldParticipant && oldParticipant->type() == Type::Admin) { return GenerateAdminChangeText( channel, user, ChatAdminRightsInfo(), oldRights); } else if (oldParticipant && oldParticipant->type() == Type::Banned) { return GeneratePermissionsChangeText( participantId, user, ChatRestrictionsInfo(), oldRestrictions); } else if (oldParticipant && oldParticipant->type() == Type::Restricted && (participant.type() == Type::Member || participant.type() == Type::Left)) { return GeneratePermissionsChangeText( participantId, user, ChatRestrictionsInfo(), oldRestrictions); } return tr::lng_admin_log_invited( tr::now, lt_user, user, Ui::Text::WithEntities); }; auto result = [&] { const auto &peerId = participant.id(); switch (participant.type()) { case Api::ChatParticipant::Type::Creator: { // No valid string here :( const auto user = GenerateParticipantString( &channel->session(), peerId); if (peerId == channel->session().userPeerId()) { return GenerateAdminChangeText( channel, user, participant.rights(), oldRights); } return tr::lng_admin_log_transferred( tr::now, lt_user, user, Ui::Text::WithEntities); } case Api::ChatParticipant::Type::Admin: { const auto user = GenerateParticipantString( &channel->session(), peerId); return GenerateAdminChangeText( channel, user, participant.rights(), oldRights); } case Api::ChatParticipant::Type::Restricted: case Api::ChatParticipant::Type::Banned: { const auto user = GenerateParticipantString( &channel->session(), peerId); return GeneratePermissionsChangeText( peerId, user, participant.restrictions(), oldRestrictions); } case Api::ChatParticipant::Type::Left: case Api::ChatParticipant::Type::Member: return generateOther(peerId); }; Unexpected("Participant type in GenerateParticipantChangeText."); }(); result.entities.push_front( EntityInText(EntityType::Italic, 0, result.text.size())); return result; } TextWithEntities GenerateParticipantChangeText( not_null channel, const MTPChannelParticipant &participant, std::optional oldParticipant = std::nullopt) { return GenerateParticipantChangeText( channel, Api::ChatParticipant(participant, channel), oldParticipant ? std::make_optional(Api::ChatParticipant( *oldParticipant, channel)) : std::nullopt); } TextWithEntities GenerateDefaultBannedRightsChangeText( not_null channel, ChatRestrictionsInfo rights, ChatRestrictionsInfo oldRights) { auto result = TextWithEntities{ tr::lng_admin_log_changed_default_permissions(tr::now) }; const auto changes = GeneratePermissionsChangeText(rights, oldRights); if (!changes.isEmpty()) { result.text.append('\n' + changes); } result.entities.push_front( EntityInText(EntityType::Italic, 0, result.text.size())); return result; } [[nodiscard]] bool IsTopicClosed(const MTPForumTopic &topic) { return topic.match([](const MTPDforumTopic &data) { return data.is_closed(); }, [](const MTPDforumTopicDeleted &) { return false; }); } [[nodiscard]] bool IsTopicHidden(const MTPForumTopic &topic) { return topic.match([](const MTPDforumTopic &data) { return data.is_hidden(); }, [](const MTPDforumTopicDeleted &) { return false; }); } [[nodiscard]] TextWithEntities GenerateTopicLink( not_null channel, const MTPForumTopic &topic) { return topic.match([&](const MTPDforumTopic &data) { return Ui::Text::Link( Data::ForumTopicIconWithTitle( data.vid().v, data.vicon_emoji_id().value_or_empty(), qs(data.vtitle())), u"internal:url:https://t.me/c/%1/%2"_q.arg( peerToChannel(channel->id).bare).arg( data.vid().v)); }, [](const MTPDforumTopicDeleted &) { return TextWithEntities{u"Deleted"_q}; }); } } // namespace OwnedItem::OwnedItem(std::nullptr_t) { } OwnedItem::OwnedItem( not_null delegate, not_null data) : _data(data), _view(_data->createView(delegate)) { } OwnedItem::OwnedItem(OwnedItem &&other) : _data(base::take(other._data)), _view(base::take(other._view)) { } OwnedItem &OwnedItem::operator=(OwnedItem &&other) { _data = base::take(other._data); _view = base::take(other._view); return *this; } OwnedItem::~OwnedItem() { clearView(); if (_data) { _data->destroy(); } } void OwnedItem::refreshView( not_null delegate) { _view = _data->createView(delegate); } void OwnedItem::clearView() { _view = nullptr; } void GenerateItems( not_null delegate, not_null history, EditedMessage message, Fn callback) { PeerData *from = history->owner().userLoaded(message.fromId); if (!from) { from = history->owner().channelLoaded(message.fromId); } if (!from) { from = reinterpret_cast(history->owner().chatLoaded(message.fromId)); } if (!from) { return; } const auto date = message.entityCreateDate; const auto addPart = [&]( not_null item, TimeId sentDate = 0, MsgId realId = MsgId()) { return callback(OwnedItem(delegate, item), sentDate, realId); }; const auto fromName = from->name(); const auto fromLink = from->createOpenLink(); const auto fromLinkText = Ui::Text::Link(fromName, QString()); const auto makeSimpleTextMessage = [&](TextWithEntities &&text) { return history->makeMessage({ .id = history->nextNonHistoryEntryId(), .flags = MessageFlag::HasFromId | MessageFlag::AdminLogEntry, .from = from->id, .date = date, }, std::move(text), MTP_messageMediaEmpty()); }; const auto addSimpleTextMessage = [&](TextWithEntities &&text) { addPart(makeSimpleTextMessage(std::move(text))); }; const auto text = QString::fromStdString(message.text); addSimpleTextMessage(Ui::Text::WithEntities(text)); } } // namespace EditedLog