Files
tdesktop/Telegram/SourceFiles/calls/group/calls_group_messages.cpp
allhaileris afb81b8278
Some checks failed
Docker. / Ubuntu (push) Has been cancelled
User-agent updater. / User-agent (push) Failing after 15s
Lock Threads / lock (push) Failing after 10s
Waiting for answer. / waiting-for-answer (push) Failing after 22s
Close stale issues and PRs / stale (push) Successful in 13s
Needs user action. / needs-user-action (push) Failing after 8s
Can't reproduce. / cant-reproduce (push) Failing after 8s
init
2026-02-16 15:50:16 +03:00

732 lines
19 KiB
C++

/*
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 "calls/group/calls_group_messages.h"
#include "apiwrap.h"
#include "api/api_blocked_peers.h"
#include "api/api_chat_participants.h"
#include "api/api_text_entities.h"
#include "base/random.h"
#include "base/unixtime.h"
#include "calls/group/ui/calls_group_stars_coloring.h"
#include "calls/group/calls_group_call.h"
#include "calls/group/calls_group_message_encryption.h"
#include "data/data_channel.h"
#include "data/data_group_call.h"
#include "data/data_message_reactions.h"
#include "data/data_peer.h"
#include "data/data_session.h"
#include "data/data_user.h"
#include "main/main_app_config.h"
#include "main/main_session.h"
#include "mtproto/sender.h"
#include "ui/text/text_utilities.h"
#include "ui/ui_utility.h"
namespace Calls::Group {
namespace {
constexpr auto kMaxShownVideoStreamMessages = 100;
constexpr auto kStarsStatsShortPollDelay = 30 * crl::time(1000);
[[nodiscard]] StarsTop ParseStarsTop(
not_null<Data::Session*> owner,
const MTPphone_GroupCallStars &stars) {
const auto &data = stars.data();
const auto &list = data.vtop_donors().v;
auto result = StarsTop{ .total = int(data.vtotal_stars().v) };
result.topDonors.reserve(list.size());
for (const auto &entry : list) {
const auto &fields = entry.data();
result.topDonors.push_back({
.peer = (fields.vpeer_id()
? owner->peer(peerFromMTP(*fields.vpeer_id())).get()
: nullptr),
.stars = int(fields.vstars().v),
.my = fields.is_my(),
});
}
return result;
}
[[nodiscard]] TimeId PinFinishDate(
not_null<PeerData*> peer,
TimeId date,
int stars) {
if (!date || !stars) {
return 0;
}
const auto &colorings = peer->session().appConfig().groupCallColorings();
return date + Ui::StarsColoringForCount(colorings, stars).secondsPin;
}
[[nodiscard]] TimeId PinFinishDate(const Message &message) {
return PinFinishDate(message.peer, message.date, message.stars);
}
} // namespace
Messages::Messages(not_null<GroupCall*> call, not_null<MTP::Sender*> api)
: _call(call)
, _session(&call->peer()->session())
, _api(api)
, _destroyTimer([=] { checkDestroying(); })
, _ttl(_session->appConfig().groupCallMessageTTL())
, _starsStatsTimer([=] { requestStarsStats(); }) {
Ui::PostponeCall(_call, [=] {
_call->real(
) | rpl::on_next([=](not_null<Data::GroupCall*> call) {
_real = call;
if (ready()) {
sendPending();
} else {
Unexpected("Not ready call.");
}
}, _lifetime);
requestStarsStats();
});
}
Messages::~Messages() {
if (_paid.sending > 0) {
finishPaidSending({
.count = int(_paid.sending),
.valid = true,
.shownPeer = _paid.sendingShownPeer,
}, false);
}
}
void Messages::requestStarsStats() {
if (!_call->videoStream()) {
return;
}
_starsStatsTimer.cancel();
_starsTopRequestId = _api->request(MTPphone_GetGroupCallStars(
_call->inputCall()
)).done([=](const MTPphone_GroupCallStars &result) {
const auto &data = result.data();
const auto owner = &_session->data();
owner->processUsers(data.vusers());
owner->processChats(data.vchats());
_paid.top = ParseStarsTop(owner, result);
_paidChanges.fire({});
_starsStatsTimer.callOnce(kStarsStatsShortPollDelay);
}).fail([=](const MTP::Error &error) {
[[maybe_unused]] const auto &type = error.type();
_starsStatsTimer.callOnce(kStarsStatsShortPollDelay);
}).send();
}
bool Messages::ready() const {
return _real && (!_call->conference() || _call->e2eEncryptDecrypt());
}
void Messages::send(TextWithTags text, int stars) {
if (text.empty() && !stars) {
return;
} else if (!ready()) {
_pending.push_back({ std::move(text), stars });
return;
}
auto prepared = TextWithEntities{
text.text,
TextUtilities::ConvertTextTagsToEntities(text.tags)
};
auto serialized = MTPTextWithEntities(MTP_textWithEntities(
MTP_string(prepared.text),
Api::EntitiesToMTP(
&_real->session(),
prepared.entities,
Api::ConvertOption::SkipLocal)));
const auto localId = _call->peer()->owner().nextLocalMessageId();
const auto randomId = base::RandomValue<uint64>();
_sendingIdByRandomId.emplace(randomId, localId);
const auto from = _call->messagesFrom();
const auto creator = _real->creator();
const auto skip = skipMessage(prepared, stars);
if (skip) {
_skippedIds.emplace(localId);
} else {
_messages.push_back({
.id = localId,
.peer = from,
.text = std::move(prepared),
.stars = stars,
.admin = (from == _call->peer()) || (creator && from->isSelf()),
.mine = true,
});
}
if (!_call->conference()) {
using Flag = MTPphone_SendGroupCallMessage::Flag;
_api->request(MTPphone_SendGroupCallMessage(
MTP_flags(Flag::f_send_as
| (stars ? Flag::f_allow_paid_stars : Flag())),
_call->inputCall(),
MTP_long(randomId),
serialized,
MTP_long(stars),
from->input()
)).done([=](
const MTPUpdates &result,
const MTP::Response &response) {
_session->api().applyUpdates(result, randomId);
}).fail([=](const MTP::Error &, const MTP::Response &response) {
failed(randomId, response);
}).send();
} else {
const auto bytes = SerializeMessage({ randomId, serialized });
auto v = std::vector<std::uint8_t>(bytes.size());
bytes::copy(bytes::make_span(v), bytes::make_span(bytes));
const auto userId = peerToUser(from->id).bare;
const auto encrypt = _call->e2eEncryptDecrypt();
const auto encrypted = encrypt(v, int64_t(userId), true, 0);
_api->request(MTPphone_SendGroupCallEncryptedMessage(
_call->inputCall(),
MTP_bytes(bytes::make_span(encrypted))
)).done([=](const MTPBool &, const MTP::Response &response) {
sent(randomId, response);
}).fail([=](const MTP::Error &, const MTP::Response &response) {
failed(randomId, response);
}).send();
}
addStars(from, stars, true);
if (!skip) {
checkDestroying(true);
}
}
void Messages::setApplyingInitial(bool value) {
_applyingInitial = value;
}
void Messages::received(const MTPDupdateGroupCallMessage &data) {
if (!ready()) {
return;
}
const auto &fields = data.vmessage().data();
received(
fields.vid().v,
fields.vfrom_id(),
fields.vmessage(),
fields.vdate().v,
fields.vpaid_message_stars().value_or_empty(),
fields.is_from_admin());
}
void Messages::received(const MTPDupdateGroupCallEncryptedMessage &data) {
if (!ready()) {
return;
}
const auto fromId = data.vfrom_id();
const auto &bytes = data.vencrypted_message().v;
auto v = std::vector<std::uint8_t>(bytes.size());
bytes::copy(bytes::make_span(v), bytes::make_span(bytes));
const auto userId = peerToUser(peerFromMTP(fromId)).bare;
const auto decrypt = _call->e2eEncryptDecrypt();
const auto decrypted = decrypt(v, int64_t(userId), false, 0);
const auto deserialized = DeserializeMessage(QByteArray::fromRawData(
reinterpret_cast<const char*>(decrypted.data()),
decrypted.size()));
if (!deserialized) {
LOG(("API Error: Can't parse decrypted message"));
return;
}
const auto realId = ++_conferenceIdAutoIncrement;
const auto randomId = deserialized->randomId;
if (!_conferenceIdByRandomId.emplace(randomId, realId).second) {
// Already received.
return;
}
received(
realId,
fromId,
deserialized->message,
base::unixtime::now(), // date
0, // stars
false,
true); // checkCustomEmoji
}
void Messages::deleted(const MTPDupdateDeleteGroupCallMessages &data) {
const auto was = _messages.size();
for (const auto &id : data.vmessages().v) {
const auto i = ranges::find(_messages, id.v, &Message::id);
if (i != end(_messages)) {
_messages.erase(i);
}
}
if (_messages.size() < was) {
pushChanges();
}
}
void Messages::sent(const MTPDupdateMessageID &data) {
sent(data.vrandom_id().v, data.vid().v);
}
void Messages::sent(uint64 randomId, const MTP::Response &response) {
const auto realId = ++_conferenceIdAutoIncrement;
_conferenceIdByRandomId.emplace(randomId, realId);
sent(randomId, realId);
const auto i = ranges::find(_messages, realId, &Message::id);
if (i != end(_messages) && !i->date) {
i->date = Api::UnixtimeFromMsgId(response.outerMsgId);
i->pinFinishDate = PinFinishDate(*i);
checkDestroying(true);
}
}
void Messages::sent(uint64 randomId, MsgId realId) {
const auto i = _sendingIdByRandomId.find(randomId);
if (i == end(_sendingIdByRandomId)) {
return;
}
const auto localId = i->second;
_sendingIdByRandomId.erase(i);
const auto j = ranges::find(_messages, localId, &Message::id);
if (j == end(_messages)) {
_skippedIds.emplace(realId);
return;
}
j->id = realId;
crl::on_main(this, [=] {
const auto i = ranges::find(_messages, realId, &Message::id);
if (i != end(_messages) && !i->date) {
i->date = base::unixtime::now();
i->pinFinishDate = PinFinishDate(*i);
checkDestroying(true);
}
});
_idUpdates.fire({ .localId = localId, .realId = realId });
}
void Messages::received(
MsgId id,
const MTPPeer &from,
const MTPTextWithEntities &message,
TimeId date,
int stars,
bool fromAdmin,
bool checkCustomEmoji) {
const auto peer = _call->peer();
const auto i = ranges::find(_messages, id, &Message::id);
if (i != end(_messages)) {
const auto fromId = peerFromMTP(from);
const auto me1 = peer->session().userPeerId();
const auto me2 = _call->messagesFrom()->id;
if (((fromId == me1) || (fromId == me2)) && !i->date) {
i->date = date;
i->pinFinishDate = PinFinishDate(*i);
checkDestroying(true);
}
return;
} else if (_skippedIds.contains(id)) {
return;
}
auto allowedEntityTypes = std::vector<EntityType>{
EntityType::Code,
EntityType::Bold,
EntityType::Semibold,
EntityType::Spoiler,
EntityType::StrikeOut,
EntityType::Underline,
EntityType::Italic,
EntityType::CustomEmoji,
};
if (checkCustomEmoji && !peer->isSelf() && !peer->isPremium()) {
allowedEntityTypes.pop_back();
}
const auto author = peer->owner().peer(peerFromMTP(from));
auto text = Ui::Text::Filtered(
Api::ParseTextWithEntities(&author->session(), message),
allowedEntityTypes);
const auto mine = author->isSelf()
|| (author->isChannel() && author->asChannel()->amCreator());
const auto skip = skipMessage(text, stars);
if (skip) {
_skippedIds.emplace(id);
} else {
// Should check by sendAsPeers() list instead, but it may not be
// loaded here yet.
_messages.push_back({
.id = id,
.date = date,
.pinFinishDate = PinFinishDate(author, date, stars),
.peer = author,
.text = std::move(text),
.stars = stars,
.admin = fromAdmin,
.mine = mine,
});
ranges::sort(_messages, ranges::less(), &Message::id);
}
if (!_applyingInitial) {
addStars(author, stars, mine);
}
if (!skip) {
checkDestroying(true);
}
}
bool Messages::skipMessage(const TextWithEntities &text, int stars) const {
const auto real = _call->lookupReal();
return text.empty()
&& real
&& (stars < real->messagesMinPrice());
}
void Messages::checkDestroying(bool afterChanges) {
auto next = TimeId();
const auto now = base::unixtime::now();
const auto initial = int(_messages.size());
if (_call->videoStream()) {
if (initial > kMaxShownVideoStreamMessages) {
const auto remove = initial - kMaxShownVideoStreamMessages;
auto i = begin(_messages);
for (auto k = 0; k != remove; ++k) {
if (i->date && i->pinFinishDate <= now) {
i = _messages.erase(i);
} else if (!next || next > i->pinFinishDate - now) {
next = i->pinFinishDate - now;
++i;
} else {
++i;
}
}
}
} else for (auto i = begin(_messages); i != end(_messages);) {
const auto date = i->date;
//const auto ttl = i->stars
// ? (Ui::StarsColoringForCount(i->stars).minutesPin * 60)
// : _ttl;
const auto ttl = _ttl;
if (!date) {
if (i->id < 0) {
++i;
} else {
i = _messages.erase(i);
}
} else if (date + ttl <= now) {
i = _messages.erase(i);
} else if (!next || next > date + ttl - now) {
next = date + ttl - now;
++i;
} else {
++i;
}
}
if (!next) {
_destroyTimer.cancel();
} else {
const auto delay = next * crl::time(1000);
if (!_destroyTimer.isActive()
|| (_destroyTimer.remainingTime() > delay)) {
_destroyTimer.callOnce(delay);
}
}
if (afterChanges || (_messages.size() < initial)) {
pushChanges();
}
}
rpl::producer<std::vector<Message>> Messages::listValue() const {
return _changes.events_starting_with_copy(_messages);
}
rpl::producer<MessageIdUpdate> Messages::idUpdates() const {
return _idUpdates.events();
}
void Messages::sendPending() {
Expects(_real != nullptr);
for (auto &pending : base::take(_pending)) {
send(std::move(pending.text), pending.stars);
}
if (_paidSendingPending) {
reactionsPaidSend();
}
}
void Messages::pushChanges() {
if (_changesScheduled) {
return;
}
_changesScheduled = true;
Ui::PostponeCall(this, [=] {
_changesScheduled = false;
_changes.fire_copy(_messages);
});
}
void Messages::failed(uint64 randomId, const MTP::Response &response) {
const auto i = _sendingIdByRandomId.find(randomId);
if (i == end(_sendingIdByRandomId)) {
return;
}
const auto localId = i->second;
_sendingIdByRandomId.erase(i);
const auto j = ranges::find(_messages, localId, &Message::id);
if (j != end(_messages) && !j->date) {
j->date = Api::UnixtimeFromMsgId(response.outerMsgId);
j->stars = 0;
j->failed = true;
checkDestroying(true);
}
}
int Messages::reactionsPaidScheduled() const {
return _paid.scheduled;
}
PeerId Messages::reactionsLocalShownPeer() const {
const auto minePaidShownPeer = [&] {
for (const auto &entry : _paid.top.topDonors) {
if (entry.my) {
return entry.peer ? entry.peer->id : PeerId();
}
}
return _call->messagesFrom()->id;
//const auto api = &_session->api();
//return api->globalPrivacy().paidReactionShownPeerCurrent();
};
return _paid.scheduledFlag
? _paid.scheduledShownPeer
: _paid.sendingFlag
? _paid.sendingShownPeer
: minePaidShownPeer();
}
void Messages::reactionsPaidAdd(int count) {
Expects(count >= 0);
_paid.scheduled += count;
_paid.scheduledFlag = 1;
_paid.scheduledShownPeer = _call->messagesFrom()->id;
if (count > 0) {
_session->credits().lock(CreditsAmount(count));
}
_call->peer()->owner().reactions().schedulePaid(_call);
_paidChanges.fire({});
}
void Messages::reactionsPaidScheduledCancel() {
if (!_paid.scheduledFlag) {
return;
}
if (const auto amount = int(_paid.scheduled)) {
_session->credits().unlock(
CreditsAmount(amount));
}
_paid.scheduled = 0;
_paid.scheduledFlag = 0;
_paid.scheduledShownPeer = 0;
_paidChanges.fire({});
}
Data::PaidReactionSend Messages::startPaidReactionSending() {
_paidSendingPending = false;
if (!_paid.scheduledFlag || !_paid.scheduled) {
return {};
} else if (_paid.sendingFlag || !ready()) {
_paidSendingPending = true;
return {};
}
_paid.sending = _paid.scheduled;
_paid.sendingFlag = _paid.scheduledFlag;
_paid.sendingShownPeer = _paid.scheduledShownPeer;
_paid.scheduled = 0;
_paid.scheduledFlag = 0;
_paid.scheduledShownPeer = 0;
return {
.count = int(_paid.sending),
.valid = true,
.shownPeer = _paid.sendingShownPeer,
};
}
void Messages::finishPaidSending(
Data::PaidReactionSend send,
bool success) {
Expects(send.count == _paid.sending);
Expects(send.valid == (_paid.sendingFlag == 1));
Expects(send.shownPeer == _paid.sendingShownPeer);
_paid.sending = 0;
_paid.sendingFlag = 0;
_paid.sendingShownPeer = 0;
if (const auto amount = send.count) {
if (success) {
const auto from = _session->data().peer(*send.shownPeer);
_session->credits().withdrawLocked(CreditsAmount(amount));
auto &donors = _paid.top.topDonors;
const auto i = ranges::find(donors, true, &StarsDonor::my);
if (i != end(donors)) {
i->peer = from;
i->stars += amount;
} else {
donors.push_back({
.peer = from,
.stars = amount,
.my = true,
});
}
} else {
_session->credits().unlock(CreditsAmount(amount));
_paidChanges.fire({});
}
}
if (_paidSendingPending) {
reactionsPaidSend();
}
}
void Messages::reactionsPaidSend() {
const auto send = startPaidReactionSending();
if (!send.valid || !send.count) {
return;
}
const auto localId = _call->peer()->owner().nextLocalMessageId();
const auto randomId = base::RandomValue<uint64>();
_sendingIdByRandomId.emplace(randomId, localId);
const auto from = _session->data().peer(*send.shownPeer);
const auto stars = int(send.count);
const auto skip = skipMessage({}, stars);
if (skip) {
_skippedIds.emplace(localId);
} else {
_messages.push_back({
.id = localId,
.peer = from,
.stars = stars,
.admin = (from == _call->peer()),
.mine = true,
});
}
using Flag = MTPphone_SendGroupCallMessage::Flag;
_api->request(MTPphone_SendGroupCallMessage(
MTP_flags(Flag::f_send_as | Flag::f_allow_paid_stars),
_call->inputCall(),
MTP_long(randomId),
MTP_textWithEntities(MTP_string(), MTP_vector<MTPMessageEntity>()),
MTP_long(stars),
from->input()
)).done([=](
const MTPUpdates &result,
const MTP::Response &response) {
finishPaidSending(send, true);
_session->api().applyUpdates(result, randomId);
}).fail([=](const MTP::Error &, const MTP::Response &response) {
finishPaidSending(send, false);
failed(randomId, response);
}).send();
addStars(from, stars, true);
if (!skip) {
checkDestroying(true);
}
}
void Messages::undoScheduledPaidOnDestroy() {
_call->peer()->owner().reactions().undoScheduledPaid(_call);
}
Messages::PaidLocalState Messages::starsLocalState() const {
const auto &donors = _paid.top.topDonors;
const auto i = ranges::find(donors, true, &StarsDonor::my);
const auto local = int(_paid.scheduled);
const auto my = (i != end(donors) ? i->stars : 0) + local;
const auto total = _paid.top.total + local;
return { .total = total, .my = my };
}
void Messages::deleteConfirmed(MessageDeleteRequest request) {
const auto eraseFrom = [&](auto iterator) {
if (iterator != end(_messages)) {
_messages.erase(iterator, end(_messages));
pushChanges();
}
};
const auto peer = _call->peer();
if (const auto from = request.deleteAllFrom) {
using Flag = MTPphone_DeleteGroupCallParticipantMessages::Flag;
_api->request(MTPphone_DeleteGroupCallParticipantMessages(
MTP_flags(request.reportSpam ? Flag::f_report_spam : Flag()),
_call->inputCall(),
from->input()
)).send();
eraseFrom(ranges::remove(_messages, not_null(from), &Message::peer));
} else {
using Flag = MTPphone_DeleteGroupCallMessages::Flag;
_api->request(MTPphone_DeleteGroupCallMessages(
MTP_flags(request.reportSpam ? Flag::f_report_spam : Flag()),
_call->inputCall(),
MTP_vector<MTPint>(1, MTP_int(request.id.bare))
)).send();
eraseFrom(ranges::remove(_messages, request.id, &Message::id));
}
if (const auto ban = request.ban) {
if (const auto channel = peer->asChannel()) {
ban->session().api().chatParticipants().kick(
channel,
ban,
ChatRestrictionsInfo());
} else {
ban->session().api().blockedPeers().block(ban);
}
}
}
void Messages::addStars(not_null<PeerData*> from, int stars, bool mine) {
if (stars <= 0) {
return;
}
_paid.top.total += stars;
const auto i = ranges::find(
_paid.top.topDonors,
from.get(),
&StarsDonor::peer);
if (i != end(_paid.top.topDonors)) {
i->stars += stars;
} else {
_paid.top.topDonors.push_back({
.peer = from,
.stars = stars,
.my = mine,
});
}
ranges::stable_sort(
_paid.top.topDonors,
ranges::greater(),
&StarsDonor::stars);
_paidChanges.fire({ .peer = from, .stars = stars });
}
} // namespace Calls::Group