init
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
Needs user action. / needs-user-action (push) Failing after 8s
Can't reproduce. / cant-reproduce (push) Failing after 8s
Close stale issues and PRs / stale (push) Has been cancelled

This commit is contained in:
allhaileris
2026-02-16 15:50:16 +03:00
commit afb81b8278
13816 changed files with 3689732 additions and 0 deletions

View File

@@ -0,0 +1,92 @@
/*
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 "api/api_attached_stickers.h"
#include "apiwrap.h"
#include "ui/boxes/confirm_box.h"
#include "boxes/sticker_set_box.h"
#include "boxes/stickers_box.h"
#include "data/data_document.h"
#include "data/data_photo.h"
#include "lang/lang_keys.h"
#include "window/window_session_controller.h"
namespace Api {
AttachedStickers::AttachedStickers(not_null<ApiWrap*> api)
: _api(&api->instance()) {
}
void AttachedStickers::request(
not_null<Window::SessionController*> controller,
MTPmessages_GetAttachedStickers &&mtpRequest) {
const auto weak = base::make_weak(controller);
_api.request(_requestId).cancel();
_requestId = _api.request(
std::move(mtpRequest)
).done([=](const MTPVector<MTPStickerSetCovered> &result) {
_requestId = 0;
const auto strongController = weak.get();
if (!strongController) {
return;
}
if (result.v.isEmpty()) {
strongController->show(
Ui::MakeInformBox(tr::lng_stickers_not_found()));
return;
} else if (result.v.size() > 1) {
strongController->show(
Box<StickersBox>(strongController->uiShow(), result.v));
return;
}
// Single attached sticker pack.
const auto data = result.v.front().match([&](const auto &data) {
return &data.vset().data();
});
const auto setId = (data->vid().v && data->vaccess_hash().v)
? StickerSetIdentifier{
.id = data->vid().v,
.accessHash = data->vaccess_hash().v }
: StickerSetIdentifier{ .shortName = qs(data->vshort_name()) };
strongController->show(Box<StickerSetBox>(
strongController->uiShow(),
setId,
(data->is_emojis()
? Data::StickersType::Emoji
: data->is_masks()
? Data::StickersType::Masks
: Data::StickersType::Stickers)));
}).fail([=] {
_requestId = 0;
if (const auto strongController = weak.get()) {
strongController->show(
Ui::MakeInformBox(tr::lng_stickers_not_found()));
}
}).send();
}
void AttachedStickers::requestAttachedStickerSets(
not_null<Window::SessionController*> controller,
not_null<PhotoData*> photo) {
request(
controller,
MTPmessages_GetAttachedStickers(
MTP_inputStickeredMediaPhoto(photo->mtpInput())));
}
void AttachedStickers::requestAttachedStickerSets(
not_null<Window::SessionController*> controller,
not_null<DocumentData*> document) {
request(
controller,
MTPmessages_GetAttachedStickers(
MTP_inputStickeredMediaDocument(document->mtpInput())));
}
} // namespace Api

View File

@@ -0,0 +1,44 @@
/*
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
*/
#pragma once
#include "mtproto/sender.h"
class ApiWrap;
class DocumentData;
class PhotoData;
namespace Window {
class SessionController;
} // namespace Window
namespace Api {
class AttachedStickers final {
public:
explicit AttachedStickers(not_null<ApiWrap*> api);
void requestAttachedStickerSets(
not_null<Window::SessionController*> controller,
not_null<PhotoData*> photo);
void requestAttachedStickerSets(
not_null<Window::SessionController*> controller,
not_null<DocumentData*> document);
private:
void request(
not_null<Window::SessionController*> controller,
MTPmessages_GetAttachedStickers &&mtpRequest);
MTP::Sender _api;
mtpRequestId _requestId = 0;
};
} // namespace Api

View File

@@ -0,0 +1,405 @@
/*
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 "api/api_authorizations.h"
#include "apiwrap.h"
#include "base/unixtime.h"
#include "core/application.h"
#include "core/changelogs.h"
#include "core/core_settings.h"
#include "lang/lang_keys.h"
#include "main/main_app_config.h"
#include "main/main_session_settings.h"
#include "main/main_session.h"
namespace Api {
namespace {
constexpr auto TestApiId = 17349;
constexpr auto SnapApiId = 611335;
constexpr auto DesktopApiId = 2040;
Authorizations::Entry ParseEntry(const MTPDauthorization &data) {
auto result = Authorizations::Entry();
result.hash = data.is_current() ? 0 : data.vhash().v;
result.incomplete = data.is_password_pending();
result.callsDisabled = data.is_call_requests_disabled();
const auto apiId = result.apiId = data.vapi_id().v;
const auto isTest = (apiId == TestApiId);
const auto isDesktop = (apiId == DesktopApiId)
|| (apiId == SnapApiId)
|| isTest;
const auto appName = isDesktop
? u"Telegram Desktop%1"_q.arg(isTest ? " (GitHub)" : QString())
: qs(data.vapp_name());// + u" for "_q + qs(d.vplatform());
const auto appVer = [&] {
const auto version = qs(data.vapp_version());
if (isDesktop) {
const auto verInt = version.toInt();
if (version == QString::number(verInt)) {
return Core::FormatVersionDisplay(verInt);
}
} else {
if (const auto index = version.indexOf('('); index >= 0) {
return version.mid(index);
}
}
return version;
}();
result.name = result.hash
? qs(data.vdevice_model())
: Core::App().settings().deviceModel();
const auto country = qs(data.vcountry());
//const auto platform = qs(data.vplatform());
//const auto &countries = countriesByISO2();
//const auto j = countries.constFind(country);
//if (j != countries.cend()) {
// country = QString::fromUtf8(j.value()->name);
//}
result.system = qs(data.vsystem_version());
result.platform = qs(data.vplatform());
result.activeTime = data.vdate_active().v
? data.vdate_active().v
: data.vdate_created().v;
result.info = QString("%1%2").arg(
appName,
appVer.isEmpty() ? QString() : (' ' + appVer));
result.ip = qs(data.vip());
result.active = result.hash
? Authorizations::ActiveDateString(result.activeTime)
: tr::lng_status_online(tr::now);
result.location = country;
return result;
}
} // namespace
Authorizations::Authorizations(not_null<ApiWrap*> api)
: _api(&api->instance())
, _autoconfirmPeriod([=] {
constexpr auto kFallbackCount = 604800;
return api->session().appConfig().get<int>(
u"authorization_autoconfirm_period"_q,
kFallbackCount);
})
, _saveUnreviewed([=] {
api->session().settings().setUnreviewed(_unreviewed);
api->session().saveSettingsDelayed();
}) {
_unreviewed = api->session().settings().unreviewed();
crl::on_main(&api->session(), [=] { removeExpiredUnreviewed(); });
Core::App().settings().deviceModelChanges(
) | rpl::on_next([=](const QString &model) {
auto changed = false;
for (auto &entry : _list) {
if (!entry.hash) {
entry.name = model;
changed = true;
}
}
if (changed) {
_listChanges.fire({});
}
}, _lifetime);
if (Core::App().settings().disableCallsLegacy()) {
toggleCallsDisabledHere(true);
}
}
void Authorizations::reload() {
if (_requestId) {
return;
}
_requestId = _api.request(MTPaccount_GetAuthorizations(
)).done([=](const MTPaccount_Authorizations &result) {
_requestId = 0;
_lastReceived = crl::now();
const auto &data = result.data();
_ttlDays = data.vauthorization_ttl_days().v;
_list = ranges::views::all(
data.vauthorizations().v
) | ranges::views::transform([](const MTPAuthorization &auth) {
return ParseEntry(auth.data());
}) | ranges::to<List>;
removeExpiredUnreviewed();
refreshCallsDisabledHereFromCloud();
_listChanges.fire({});
}).fail([=] {
_requestId = 0;
}).send();
}
void Authorizations::cancelCurrentRequest() {
_api.request(base::take(_requestId)).cancel();
}
void Authorizations::refreshCallsDisabledHereFromCloud() {
const auto that = ranges::find(_list, 0, &Entry::hash);
if (that != end(_list)
&& !_toggleCallsDisabledRequests.contains(0)) {
_callsDisabledHere = that->callsDisabled;
}
}
void Authorizations::requestTerminate(
Fn<void(const MTPBool &result)> &&done,
Fn<void(const MTP::Error &error)> &&fail,
std::optional<uint64> hash) {
const auto send = [&](auto request) {
_api.request(
std::move(request)
).done([=, done = std::move(done)](const MTPBool &result) {
done(result);
if (mtpIsTrue(result)) {
if (hash) {
_list.erase(
ranges::remove(_list, *hash, &Entry::hash),
end(_list));
} else {
_list.clear();
}
_listChanges.fire({});
}
}).fail(
std::move(fail)
).send();
};
if (hash) {
send(MTPaccount_ResetAuthorization(MTP_long(*hash)));
} else {
send(MTPauth_ResetAuthorizations());
}
}
Authorizations::List Authorizations::list() const {
return _list;
}
auto Authorizations::listValue() const
-> rpl::producer<Authorizations::List> {
return rpl::single(
list()
) | rpl::then(
_listChanges.events() | rpl::map([=] { return list(); })
);
}
rpl::producer<int> Authorizations::totalValue() const {
return rpl::single(
total()
) | rpl::then(
_listChanges.events() | rpl::map([=] { return total(); })
);
}
void Authorizations::updateTTL(int days) {
_api.request(_ttlRequestId).cancel();
_ttlRequestId = _api.request(MTPaccount_SetAuthorizationTTL(
MTP_int(days)
)).done([=] {
_ttlRequestId = 0;
}).fail([=] {
_ttlRequestId = 0;
}).send();
_ttlDays = days;
}
rpl::producer<int> Authorizations::ttlDays() const {
return _ttlDays.value() | rpl::filter(rpl::mappers::_1 != 0);
}
void Authorizations::toggleCallsDisabled(uint64 hash, bool disabled) {
if (const auto sent = _toggleCallsDisabledRequests.take(hash)) {
_api.request(*sent).cancel();
}
using Flag = MTPaccount_ChangeAuthorizationSettings::Flag;
const auto id = _api.request(MTPaccount_ChangeAuthorizationSettings(
MTP_flags(Flag::f_call_requests_disabled),
MTP_long(hash),
MTPBool(), // encrypted_requests_disabled
MTP_bool(disabled)
)).done([=] {
_toggleCallsDisabledRequests.remove(hash);
}).fail([=] {
_toggleCallsDisabledRequests.remove(hash);
}).send();
_toggleCallsDisabledRequests.emplace(hash, id);
if (!hash) {
_callsDisabledHere = disabled;
}
}
bool Authorizations::callsDisabledHere() const {
return _callsDisabledHere.current();
}
rpl::producer<bool> Authorizations::callsDisabledHereValue() const {
return _callsDisabledHere.value();
}
rpl::producer<bool> Authorizations::callsDisabledHereChanges() const {
return _callsDisabledHere.changes();
}
QString Authorizations::ActiveDateString(TimeId active) {
const auto now = QDateTime::currentDateTime();
const auto lastTime = base::unixtime::parse(active);
const auto nowDate = now.date();
const auto lastDate = lastTime.date();
return (lastDate == nowDate)
? QLocale().toString(lastTime.time(), QLocale::ShortFormat)
: (lastDate.year() == nowDate.year()
&& lastDate.weekNumber() == nowDate.weekNumber())
? langDayOfWeek(lastDate)
: QLocale().toString(lastDate, QLocale::ShortFormat);
}
int Authorizations::total() const {
return ranges::count_if(
_list,
ranges::not_fn(&Entry::incomplete));
}
crl::time Authorizations::lastReceivedTime() {
return _lastReceived;
}
const std::vector<Data::UnreviewedAuth> &Authorizations::unreviewed() {
removeExpiredUnreviewed();
return _unreviewed;
}
void Authorizations::removeExpiredUnreviewed() {
const auto now = base::unixtime::now();
const auto period = _autoconfirmPeriod();
const auto oldSize = _unreviewed.size();
_unreviewed.erase(
std::remove_if(_unreviewed.begin(), _unreviewed.end(),
[=](const auto &auth) {
return (now - auth.date) >= period;
}),
_unreviewed.end());
if (_unreviewed.size() != oldSize) {
_saveUnreviewed();
}
}
void Authorizations::review(const std::vector<uint64> &hashes, bool confirm) {
for (const auto hash : hashes) {
if (const auto sent = _reviewRequests.take(hash)) {
_api.request(*sent).cancel();
}
}
const auto checkComplete = [=] {
if (_reviewRequests.empty()) {
_saveUnreviewed();
_unreviewedChanges.fire({});
}
};
for (const auto hash : hashes) {
const auto removeFromUnreviewed = [=] {
_unreviewed.erase(
std::remove_if(_unreviewed.begin(), _unreviewed.end(),
[hash](const auto &auth) { return auth.hash == hash; }),
_unreviewed.end());
_reviewRequests.remove(hash);
checkComplete();
};
if (confirm) {
using Flag = MTPaccount_ChangeAuthorizationSettings::Flag;
const auto id = _api.request(MTPaccount_ChangeAuthorizationSettings(
MTP_flags(Flag::f_confirmed),
MTP_long(hash),
MTPBool(), // encrypted_requests_disabled
MTPBool() // call_requests_disabled
)).done([=] {
removeFromUnreviewed();
}).fail([=] {
removeFromUnreviewed();
}).send();
_reviewRequests.emplace(hash, id);
} else {
const auto id = _api.request(MTPaccount_ResetAuthorization(
MTP_long(hash)
)).done([=](const MTPBool &result) {
if (mtpIsTrue(result)) {
_list.erase(
ranges::remove(_list, hash, &Entry::hash),
end(_list));
_listChanges.fire({});
}
removeFromUnreviewed();
}).fail([=] {
removeFromUnreviewed();
}).send();
_reviewRequests.emplace(hash, id);
}
}
}
rpl::producer<> Authorizations::unreviewedChanges() const {
return _unreviewedChanges.events();
}
void Authorizations::apply(const MTPUpdate &update) {
removeExpiredUnreviewed();
update.match([&](const MTPDupdateNewAuthorization &data) {
auto unreviewed = Data::UnreviewedAuth{
.hash = data.vhash().v,
.unconfirmed = data.is_unconfirmed(),
.date = data.vdate().value_or_empty(),
.device = qs(data.vdevice().value_or_empty()),
.location = qs(data.vlocation().value_or_empty())
};
if (!unreviewed.unconfirmed) {
const auto hash = unreviewed.hash;
const auto was = _unreviewed.size();
_unreviewed.erase(
std::remove_if(
_unreviewed.begin(),
_unreviewed.end(),
[hash](const auto &auth) { return auth.hash == hash; }),
_unreviewed.end());
if (was != _unreviewed.size()) {
_saveUnreviewed();
_unreviewedChanges.fire({});
}
return;
}
for (auto &auth : _unreviewed) {
if (auth.hash == unreviewed.hash) {
auth = std::move(unreviewed);
_saveUnreviewed();
_unreviewedChanges.fire({});
return;
}
}
_unreviewed.push_back(std::move(unreviewed));
_saveUnreviewed();
_unreviewedChanges.fire({});
}, [](auto&&) {
Unexpected("Update in Authorizations::apply.");
});
}
} // namespace Api

View File

@@ -0,0 +1,93 @@
/*
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
*/
#pragma once
#include "data/data_authorization.h"
#include "mtproto/sender.h"
class ApiWrap;
namespace Api {
class Authorizations final {
public:
explicit Authorizations(not_null<ApiWrap*> api);
struct Entry {
uint64 hash = 0;
bool incomplete = false;
bool callsDisabled = false;
int apiId = 0;
TimeId activeTime = 0;
QString name, active, info, ip, location, system, platform;
};
using List = std::vector<Entry>;
void reload();
void cancelCurrentRequest();
void requestTerminate(
Fn<void(const MTPBool &result)> &&done,
Fn<void(const MTP::Error &error)> &&fail,
std::optional<uint64> hash = std::nullopt);
void apply(const MTPUpdate &update);
[[nodiscard]] crl::time lastReceivedTime();
[[nodiscard]] List list() const;
[[nodiscard]] rpl::producer<List> listValue() const;
[[nodiscard]] int total() const;
[[nodiscard]] rpl::producer<int> totalValue() const;
[[nodiscard]] const std::vector<Data::UnreviewedAuth> &unreviewed();
[[nodiscard]] rpl::producer<> unreviewedChanges() const;
void review(const std::vector<uint64> &hashes, bool confirm);
void updateTTL(int days);
[[nodiscard]] rpl::producer<int> ttlDays() const;
void toggleCallsDisabledHere(bool disabled) {
toggleCallsDisabled(0, disabled);
}
void toggleCallsDisabled(uint64 hash, bool disabled);
[[nodiscard]] bool callsDisabledHere() const;
[[nodiscard]] rpl::producer<bool> callsDisabledHereValue() const;
[[nodiscard]] rpl::producer<bool> callsDisabledHereChanges() const;
[[nodiscard]] static QString ActiveDateString(TimeId active);
private:
void refreshCallsDisabledHereFromCloud();
void removeExpiredUnreviewed();
MTP::Sender _api;
mtpRequestId _requestId = 0;
List _list;
rpl::event_stream<> _listChanges;
Fn<int()> _autoconfirmPeriod;
std::vector<Data::UnreviewedAuth> _unreviewed;
rpl::event_stream<> _unreviewedChanges;
Fn<void()> _saveUnreviewed;
mtpRequestId _ttlRequestId = 0;
rpl::variable<int> _ttlDays = 0;
base::flat_map<uint64, mtpRequestId> _toggleCallsDisabledRequests;
base::flat_map<uint64, mtpRequestId> _reviewRequests;
rpl::variable<bool> _callsDisabledHere;
crl::time _lastReceived = 0;
rpl::lifetime _lifetime;
};
} // namespace Api

View File

@@ -0,0 +1,226 @@
/*
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 "api/api_blocked_peers.h"
#include "apiwrap.h"
#include "base/unixtime.h"
#include "data/data_changes.h"
#include "data/data_peer.h"
#include "data/data_peer_id.h"
#include "data/data_session.h"
#include "main/main_session.h"
namespace Api {
namespace {
constexpr auto kBlockedFirstSlice = 16;
constexpr auto kBlockedPerPage = 40;
BlockedPeers::Slice TLToSlice(
const MTPcontacts_Blocked &blocked,
Data::Session &owner) {
const auto create = [&](int count, const QVector<MTPPeerBlocked> &list) {
auto slice = BlockedPeers::Slice();
slice.total = std::max(count, int(list.size()));
slice.list.reserve(list.size());
for (const auto &contact : list) {
contact.match([&](const MTPDpeerBlocked &data) {
slice.list.push_back({
.id = peerFromMTP(data.vpeer_id()),
.date = data.vdate().v,
});
});
}
return slice;
};
return blocked.match([&](const MTPDcontacts_blockedSlice &data) {
owner.processUsers(data.vusers());
owner.processChats(data.vchats());
return create(data.vcount().v, data.vblocked().v);
}, [&](const MTPDcontacts_blocked &data) {
owner.processUsers(data.vusers());
owner.processChats(data.vchats());
return create(0, data.vblocked().v);
});
}
} // namespace
BlockedPeers::BlockedPeers(not_null<ApiWrap*> api)
: _session(&api->session())
, _api(&api->instance()) {
}
bool BlockedPeers::Slice::Item::operator==(const Item &other) const {
return (id == other.id) && (date == other.date);
}
bool BlockedPeers::Slice::Item::operator!=(const Item &other) const {
return !(*this == other);
}
bool BlockedPeers::Slice::operator==(const BlockedPeers::Slice &other) const {
return (total == other.total) && (list == other.list);
}
bool BlockedPeers::Slice::operator!=(const BlockedPeers::Slice &other) const {
return !(*this == other);
}
void BlockedPeers::block(not_null<PeerData*> peer) {
if (peer->isBlocked()) {
_session->changes().peerUpdated(
peer,
Data::PeerUpdate::Flag::IsBlocked);
return;
} else if (blockAlreadySent(peer, true)) {
return;
}
const auto requestId = _api.request(MTPcontacts_Block(
MTP_flags(0),
peer->input()
)).done([=] {
const auto data = _blockRequests.take(peer);
peer->setIsBlocked(true);
if (_slice) {
_slice->list.insert(
_slice->list.begin(),
{ peer->id, base::unixtime::now() });
++_slice->total;
_changes.fire_copy(*_slice);
}
if (data) {
for (const auto &callback : data->callbacks) {
callback(false);
}
}
}).fail([=] {
if (const auto data = _blockRequests.take(peer)) {
for (const auto &callback : data->callbacks) {
callback(false);
}
}
}).send();
_blockRequests.emplace(peer, Request{
.requestId = requestId,
.blocking = true,
});
}
void BlockedPeers::unblock(
not_null<PeerData*> peer,
Fn<void(bool success)> done,
bool force) {
if (!force && !peer->isBlocked()) {
_session->changes().peerUpdated(
peer,
Data::PeerUpdate::Flag::IsBlocked);
return;
} else if (blockAlreadySent(peer, false, done)) {
return;
}
const auto requestId = _api.request(MTPcontacts_Unblock(
MTP_flags(0),
peer->input()
)).done([=] {
const auto data = _blockRequests.take(peer);
peer->setIsBlocked(false);
if (_slice) {
auto &list = _slice->list;
for (auto i = list.begin(); i != list.end(); ++i) {
if (i->id == peer->id) {
list.erase(i);
break;
}
}
if (_slice->total > list.size()) {
--_slice->total;
}
_changes.fire_copy(*_slice);
}
if (data) {
for (const auto &callback : data->callbacks) {
callback(true);
}
}
}).fail([=] {
if (const auto data = _blockRequests.take(peer)) {
for (const auto &callback : data->callbacks) {
callback(false);
}
}
}).send();
const auto i = _blockRequests.emplace(peer, Request{
.requestId = requestId,
.blocking = false,
}).first;
if (done) {
i->second.callbacks.push_back(std::move(done));
}
}
bool BlockedPeers::blockAlreadySent(
not_null<PeerData*> peer,
bool blocking,
Fn<void(bool success)> done) {
const auto i = _blockRequests.find(peer);
if (i == end(_blockRequests)) {
return false;
} else if (i->second.blocking == blocking) {
if (done) {
i->second.callbacks.push_back(std::move(done));
}
return true;
}
const auto callbacks = base::take(i->second.callbacks);
_blockRequests.erase(i);
for (const auto &callback : callbacks) {
callback(false);
}
return false;
}
void BlockedPeers::reload() {
if (_requestId) {
return;
}
request(0, [=](Slice &&slice) {
if (!_slice || *_slice != slice) {
_slice = slice;
_changes.fire(std::move(slice));
}
});
}
auto BlockedPeers::slice() -> rpl::producer<BlockedPeers::Slice> {
if (!_slice) {
reload();
}
return _slice
? _changes.events_starting_with_copy(*_slice)
: (_changes.events() | rpl::type_erased);
}
void BlockedPeers::request(int offset, Fn<void(BlockedPeers::Slice)> done) {
if (_requestId) {
return;
}
_requestId = _api.request(MTPcontacts_GetBlocked(
MTP_flags(0),
MTP_int(offset),
MTP_int(offset ? kBlockedPerPage : kBlockedFirstSlice)
)).done([=](const MTPcontacts_Blocked &result) {
_requestId = 0;
done(TLToSlice(result, _session->data()));
}).fail([=] {
_requestId = 0;
}).send();
}
} // namespace Api

View File

@@ -0,0 +1,74 @@
/*
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
*/
#pragma once
#include "mtproto/sender.h"
class ApiWrap;
namespace Main {
class Session;
} // namespace Main
namespace Api {
class BlockedPeers final {
public:
struct Slice {
struct Item {
PeerId id;
TimeId date = 0;
bool operator==(const Item &other) const;
bool operator!=(const Item &other) const;
};
QVector<Item> list;
int total = 0;
bool operator==(const Slice &other) const;
bool operator!=(const Slice &other) const;
};
explicit BlockedPeers(not_null<ApiWrap*> api);
void reload();
rpl::producer<Slice> slice();
void request(int offset, Fn<void(Slice)> done);
void block(not_null<PeerData*> peer);
void unblock(
not_null<PeerData*> peer,
Fn<void(bool success)> done = nullptr,
bool force = false);
private:
struct Request {
std::vector<Fn<void(bool success)>> callbacks;
mtpRequestId requestId = 0;
bool blocking = false;
};
[[nodiscard]] bool blockAlreadySent(
not_null<PeerData*> peer,
bool blocking,
Fn<void(bool success)> done = nullptr);
const not_null<Main::Session*> _session;
MTP::Sender _api;
base::flat_map<not_null<PeerData*>, Request> _blockRequests;
mtpRequestId _requestId = 0;
std::optional<Slice> _slice;
rpl::event_stream<Slice> _changes;
};
} // namespace Api

View File

@@ -0,0 +1,549 @@
/*
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 "api/api_bot.h"
#include "apiwrap.h"
#include "api/api_cloud_password.h"
#include "api/api_send_progress.h"
#include "api/api_suggest_post.h"
#include "boxes/share_box.h"
#include "boxes/passcode_box.h"
#include "boxes/url_auth_box.h"
#include "boxes/peers/choose_peer_box.h"
#include "lang/lang_keys.h"
#include "chat_helpers/bot_command.h"
#include "core/core_cloud_password.h"
#include "core/click_handler_types.h"
#include "data/data_changes.h"
#include "data/data_peer.h"
#include "data/data_poll.h"
#include "data/data_user.h"
#include "data/data_session.h"
#include "history/history.h"
#include "history/history_item.h"
#include "history/history_item_components.h"
#include "inline_bots/bot_attach_web_view.h"
#include "payments/payments_checkout_process.h"
#include "payments/payments_non_panel_process.h"
#include "main/main_session.h"
#include "mainwidget.h"
#include "mainwindow.h"
#include "window/window_session_controller.h"
#include "window/window_peer_menu.h"
#include "ui/boxes/confirm_box.h"
#include "ui/toast/toast.h"
#include "ui/layers/generic_box.h"
#include "ui/text/text_utilities.h"
#include <QtGui/QGuiApplication>
#include <QtGui/QClipboard>
namespace Api {
namespace {
void SendBotCallbackData(
not_null<Window::SessionController*> controller,
not_null<HistoryItem*> item,
int row,
int column,
std::optional<Core::CloudPasswordResult> password,
Fn<void()> done = nullptr,
Fn<void(const QString &)> handleError = nullptr) {
if (!item->isRegular()) {
return;
}
const auto history = item->history();
const auto session = &history->session();
const auto owner = &history->owner();
const auto api = &session->api();
const auto bot = item->getMessageBot();
const auto fullId = item->fullId();
const auto getButton = [=] {
return HistoryMessageMarkupButton::Get(owner, fullId, row, column);
};
const auto button = getButton();
if (!button || button->requestId) {
return;
}
using ButtonType = HistoryMessageMarkupButton::Type;
const auto isGame = (button->type == ButtonType::Game);
auto flags = MTPmessages_GetBotCallbackAnswer::Flags(0);
QByteArray sendData;
if (isGame) {
flags |= MTPmessages_GetBotCallbackAnswer::Flag::f_game;
} else if (button->type == ButtonType::Callback
|| button->type == ButtonType::CallbackWithPassword) {
flags |= MTPmessages_GetBotCallbackAnswer::Flag::f_data;
sendData = button->data;
}
const auto withPassword = password.has_value();
if (withPassword) {
flags |= MTPmessages_GetBotCallbackAnswer::Flag::f_password;
}
const auto weak = base::make_weak(controller);
const auto show = controller->uiShow();
button->requestId = api->request(MTPmessages_GetBotCallbackAnswer(
MTP_flags(flags),
history->peer->input(),
MTP_int(item->id),
MTP_bytes(sendData),
password ? password->result : MTP_inputCheckPasswordEmpty()
)).done([=](const MTPmessages_BotCallbackAnswer &result) {
const auto guard = gsl::finally([&] {
if (done) {
done();
}
});
const auto item = owner->message(fullId);
if (!item) {
return;
}
if (const auto button = getButton()) {
button->requestId = 0;
owner->requestItemRepaint(item);
}
const auto &data = result.data();
const auto message = data.vmessage()
? qs(*data.vmessage())
: QString();
const auto link = data.vurl() ? qs(*data.vurl()) : QString();
const auto showAlert = data.is_alert();
if (!message.isEmpty()) {
if (!show->valid()) {
return;
} else if (showAlert) {
show->showBox(Ui::MakeInformBox(message));
} else {
if (withPassword) {
show->hideLayer();
}
show->showToast(message);
}
} else if (!link.isEmpty()) {
if (!isGame) {
UrlClickHandler::Open(link);
return;
}
BotGameUrlClickHandler(bot, link).onClick({
Qt::LeftButton,
QVariant::fromValue(ClickHandlerContext{
.itemId = item->fullId(),
.sessionWindow = weak,
}),
});
session->sendProgressManager().update(
history,
Api::SendProgressType::PlayGame);
} else if (withPassword) {
show->hideLayer();
}
}).fail([=](const MTP::Error &error) {
const auto guard = gsl::finally([&] {
if (handleError) {
handleError(error.type());
}
});
const auto item = owner->message(fullId);
if (!item) {
return;
}
// Show error?
if (const auto button = getButton()) {
button->requestId = 0;
owner->requestItemRepaint(item);
}
}).send();
session->changes().messageUpdated(
item,
Data::MessageUpdate::Flag::BotCallbackSent
);
}
void HideSingleUseKeyboard(
not_null<Window::SessionController*> controller,
not_null<HistoryItem*> item) {
controller->content()->hideSingleUseKeyboard(item->fullId());
}
} // namespace
void SendBotCallbackData(
not_null<Window::SessionController*> controller,
not_null<HistoryItem*> item,
int row,
int column) {
SendBotCallbackData(controller, item, row, column, std::nullopt);
}
void SendBotCallbackDataWithPassword(
not_null<Window::SessionController*> controller,
not_null<HistoryItem*> item,
int row,
int column) {
if (!item->isRegular()) {
return;
}
const auto history = item->history();
const auto session = &history->session();
const auto owner = &history->owner();
const auto api = &session->api();
const auto fullId = item->fullId();
const auto getButton = [=] {
return HistoryMessageMarkupButton::Get(
owner,
fullId,
row,
column);
};
const auto button = getButton();
if (!button || button->requestId) {
return;
}
api->cloudPassword().reload();
const auto weak = base::make_weak(controller);
const auto show = controller->uiShow();
SendBotCallbackData(controller, item, row, column, {}, {}, [=](
const QString &error) {
auto box = PrePasswordErrorBox(
error,
session,
tr::lng_bots_password_confirm_check_about(
tr::now,
tr::marked));
if (box) {
show->showBox(std::move(box), Ui::LayerOption::CloseOther);
} else {
auto lifetime = std::make_shared<rpl::lifetime>();
button->requestId = -1;
api->cloudPassword().state(
) | rpl::take(
1
) | rpl::on_next([=](const Core::CloudPasswordState &state) mutable {
if (lifetime) {
base::take(lifetime)->destroy();
}
if (const auto button = getButton()) {
if (button->requestId == -1) {
button->requestId = 0;
}
} else {
return;
}
auto fields = PasscodeBox::CloudFields::From(state);
fields.customTitle = tr::lng_bots_password_confirm_title();
fields.customDescription
= tr::lng_bots_password_confirm_description(tr::now);
fields.customSubmitButton = tr::lng_passcode_submit();
fields.customCheckCallback = [=](
const Core::CloudPasswordResult &result,
base::weak_qptr<PasscodeBox> box) {
if (const auto button = getButton()) {
if (button->requestId) {
return;
}
} else {
return;
}
if (const auto item = owner->message(fullId)) {
const auto strongController = weak.get();
if (!strongController) {
return;
}
SendBotCallbackData(strongController, item, row, column, result, [=] {
if (box) {
box->closeBox();
}
}, [=](const QString &error) {
if (box) {
box->handleCustomCheckError(error);
}
});
}
};
auto object = Box<PasscodeBox>(session, fields);
show->showBox(std::move(object), Ui::LayerOption::CloseOther);
}, *lifetime);
}
});
}
bool SwitchInlineBotButtonReceived(
not_null<Window::SessionController*> controller,
const QByteArray &queryWithPeerTypes,
UserData *samePeerBot,
MsgId samePeerReplyTo) {
return controller->content()->notify_switchInlineBotButtonReceived(
QString::fromUtf8(queryWithPeerTypes),
samePeerBot,
samePeerReplyTo);
}
void ActivateBotCommand(ClickHandlerContext context, int row, int column) {
const auto strong = context.sessionWindow.get();
if (!strong) {
return;
}
const auto controller = not_null{ strong };
const auto item = controller->session().data().message(context.itemId);
if (!item) {
return;
}
const auto button = HistoryMessageMarkupButton::Get(
&item->history()->owner(),
item->fullId(),
row,
column);
if (!button) {
return;
}
using ButtonType = HistoryMessageMarkupButton::Type;
switch (button->type) {
case ButtonType::Default: {
// Copy string before passing it to the sending method
// because the original button can be destroyed inside.
const auto replyTo = item->isRegular()
? item->fullId()
: FullMsgId();
controller->content()->sendBotCommand({
.peer = item->history()->peer,
.command = QString(button->text),
.context = item->fullId(),
.replyTo = { replyTo },
});
} break;
case ButtonType::Callback:
case ButtonType::Game: {
SendBotCallbackData(controller, item, row, column);
} break;
case ButtonType::CallbackWithPassword: {
SendBotCallbackDataWithPassword(controller, item, row, column);
} break;
case ButtonType::Buy: {
Payments::CheckoutProcess::Start(
item,
Payments::Mode::Payment,
crl::guard(controller, [=](auto) {
controller->widget()->activate();
}),
Payments::ProcessNonPanelPaymentFormFactory(controller, item));
} break;
case ButtonType::Url: {
auto url = QString::fromUtf8(button->data);
auto skipConfirmation = false;
if (const auto bot = item->getMessageBot()) {
if (bot->isVerified()) {
skipConfirmation = true;
}
}
const auto variant = QVariant::fromValue(context);
if (skipConfirmation) {
UrlClickHandler::Open(url, variant);
} else {
HiddenUrlClickHandler::Open(url, variant);
}
} break;
case ButtonType::RequestLocation: {
HideSingleUseKeyboard(controller, item);
controller->show(
Ui::MakeInformBox(tr::lng_bot_share_location_unavailable()));
} break;
case ButtonType::RequestPhone: {
HideSingleUseKeyboard(controller, item);
const auto itemId = item->fullId();
const auto topicRootId = item->topicRootId();
const auto history = item->history();
controller->show(Ui::MakeConfirmBox({
.text = tr::lng_bot_share_phone(),
.confirmed = [=] {
controller->showPeerHistory(
history,
Window::SectionShow::Way::Forward,
ShowAtTheEndMsgId);
auto action = Api::SendAction(history);
action.clearDraft = false;
action.replyTo = {
.messageId = itemId,
.topicRootId = topicRootId,
};
history->session().api().shareContact(
history->session().user(),
action);
},
.confirmText = tr::lng_bot_share_phone_confirm(),
}));
} break;
case ButtonType::RequestPoll: {
HideSingleUseKeyboard(controller, item);
auto chosen = PollData::Flags();
auto disabled = PollData::Flags();
if (!button->data.isEmpty()) {
disabled |= PollData::Flag::Quiz;
if (button->data[0]) {
chosen |= PollData::Flag::Quiz;
}
}
const auto replyTo = FullReplyTo();
const auto suggest = SuggestOptions();
Window::PeerMenuCreatePoll(
controller,
item->history()->peer,
replyTo,
suggest,
chosen,
disabled);
} break;
case ButtonType::RequestPeer: {
HideSingleUseKeyboard(controller, item);
auto query = RequestPeerQuery();
Assert(button->data.size() == sizeof(query));
memcpy(&query, button->data.data(), sizeof(query));
const auto peer = item->history()->peer;
const auto itemId = item->id;
const auto id = int32(button->buttonId);
const auto chosen = [=](std::vector<not_null<PeerData*>> result) {
peer->session().api().request(MTPmessages_SendBotRequestedPeer(
peer->input(),
MTP_int(itemId),
MTP_int(id),
MTP_vector_from_range(
result | ranges::views::transform([](
not_null<PeerData*> peer) {
return MTPInputPeer(peer->input());
}))
)).done([=](const MTPUpdates &result) {
peer->session().api().applyUpdates(result);
}).send();
};
if (const auto bot = item->getMessageBot()) {
ShowChoosePeerBox(controller, bot, query, chosen);
} else {
LOG(("API Error: Bot not found for RequestPeer button."));
}
} break;
case ButtonType::SwitchInlineSame:
case ButtonType::SwitchInline: {
if (const auto bot = item->getMessageBot()) {
const auto fastSwitchDone = [&] {
const auto samePeer = (button->type
== ButtonType::SwitchInlineSame);
if (samePeer) {
SwitchInlineBotButtonReceived(
controller,
button->data,
bot,
item->id);
return true;
} else if (bot->isBot() && bot->botInfo->inlineReturnTo.key) {
const auto switched = SwitchInlineBotButtonReceived(
controller,
button->data);
if (switched) {
return true;
}
}
return false;
}();
if (!fastSwitchDone) {
const auto query = QString::fromUtf8(button->data);
const auto chosen = [=](not_null<Data::Thread*> thread) {
return controller->switchInlineQuery(
thread,
bot,
query);
};
Window::ShowChooseRecipientBox(
controller,
chosen,
tr::lng_inline_switch_choose(),
nullptr,
button->peerTypes);
}
}
} break;
case ButtonType::Auth:
UrlAuthBox::Activate(item, row, column);
break;
case ButtonType::UserProfile: {
const auto session = &item->history()->session();
const auto userId = UserId(button->data.toULongLong());
if (const auto user = session->data().userLoaded(userId)) {
controller->showPeerInfo(user);
}
} break;
case ButtonType::WebView: {
if (const auto bot = item->getMessageBot()) {
bot->session().attachWebView().open({
.bot = bot,
.context = { .controller = controller },
.button = { .text = button->text, .url = button->data },
.source = InlineBots::WebViewSourceButton{ .simple = false },
});
}
} break;
case ButtonType::SimpleWebView: {
if (const auto bot = item->getMessageBot()) {
bot->session().attachWebView().open({
.bot = bot,
.context = { .controller = controller },
.button = { .text = button->text, .url = button->data },
.source = InlineBots::WebViewSourceButton{ .simple = true },
});
}
} break;
case ButtonType::CopyText: {
const auto text = QString::fromUtf8(button->data);
if (!text.isEmpty()) {
QGuiApplication::clipboard()->setText(text);
controller->showToast(tr::lng_text_copied(tr::now));
}
} break;
case ButtonType::SuggestAccept: {
Api::AcceptClickHandler(item)->onClick(ClickContext{
Qt::LeftButton,
QVariant::fromValue(context),
});
} break;
case ButtonType::SuggestDecline: {
Api::DeclineClickHandler(item)->onClick(ClickContext{
Qt::LeftButton,
QVariant::fromValue(context),
});
} break;
case ButtonType::SuggestChange: {
Api::SuggestChangesClickHandler(item)->onClick(ClickContext{
Qt::LeftButton,
QVariant::fromValue(context),
});
} break;
}
}
} // namespace Api

View File

@@ -0,0 +1,39 @@
/*
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
*/
#pragma once
struct ClickHandlerContext;
class HistoryItem;
namespace Window {
class SessionController;
} // namespace Window
namespace Api {
void SendBotCallbackData(
not_null<Window::SessionController*> controller,
not_null<HistoryItem*> item,
int row,
int column);
void SendBotCallbackDataWithPassword(
not_null<Window::SessionController*> controller,
not_null<HistoryItem*> item,
int row,
int column);
bool SwitchInlineBotButtonReceived(
not_null<Window::SessionController*> controller,
const QByteArray &queryWithPeerTypes,
UserData *samePeerBot = nullptr,
MsgId samePeerReplyTo = 0);
void ActivateBotCommand(ClickHandlerContext context, int row, int column);
} // namespace Api

View File

@@ -0,0 +1,912 @@
/*
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 "api/api_chat_filters.h"
#include "api/api_text_entities.h"
#include "apiwrap.h"
#include "base/event_filter.h"
#include "boxes/peer_list_box.h"
#include "boxes/premium_limits_box.h"
#include "boxes/filters/edit_filter_links.h" // FilterChatStatusText
#include "core/application.h"
#include "core/core_settings.h"
#include "core/ui_integration.h"
#include "data/data_channel.h"
#include "data/data_chat.h"
#include "data/data_chat_filters.h"
#include "data/data_peer.h"
#include "data/data_session.h"
#include "history/history.h"
#include "lang/lang_keys.h"
#include "main/main_session.h"
#include "ui/boxes/confirm_box.h"
#include "ui/controls/filter_link_header.h"
#include "ui/text/text_utilities.h"
#include "ui/toast/toast.h"
#include "ui/widgets/buttons.h"
#include "ui/filter_icons.h"
#include "ui/vertical_list.h"
#include "ui/ui_utility.h"
#include "window/window_session_controller.h"
#include "styles/style_filter_icons.h"
#include "styles/style_layers.h"
#include "styles/style_settings.h"
namespace Api {
namespace {
enum class ToggleAction {
Adding,
Removing,
};
class ToggleChatsController final
: public PeerListController
, public base::has_weak_ptr {
public:
ToggleChatsController(
not_null<Window::SessionController*> window,
ToggleAction action,
Data::ChatFilterTitle title,
std::vector<not_null<PeerData*>> chats,
std::vector<not_null<PeerData*>> additional);
void prepare() override;
void rowClicked(not_null<PeerListRow*> row) override;
Main::Session &session() const override;
[[nodiscard]] auto selectedValue() const
-> rpl::producer<base::flat_set<not_null<PeerData*>>>;
void adjust(int minHeight, int maxHeight, int addedTopHeight);
void setRealContentHeight(rpl::producer<int> value);
rpl::producer<int> boxHeightValue() const override;
private:
void setupAboveWidget();
void setupBelowWidget();
void initDesiredHeightValue();
void toggleAllSelected(bool select);
const not_null<Window::SessionController*> _window;
Ui::RpWidget *_addedTopWidget = nullptr;
Ui::RpWidget *_addedBottomWidget = nullptr;
ToggleAction _action = ToggleAction::Adding;
base::flat_set<not_null<PeerData*>> _checkable;
std::vector<not_null<PeerData*>> _chats;
std::vector<not_null<PeerData*>> _additional;
rpl::variable<base::flat_set<not_null<PeerData*>>> _selected;
int _minTopHeight = 0;
rpl::variable<int> _maxTopHeight;
rpl::variable<int> _aboveHeight;
rpl::variable<int> _belowHeight;
rpl::variable<int> _desiredHeight;
base::unique_qptr<Ui::PopupMenu> _menu;
rpl::lifetime _lifetime;
};
[[nodiscard]] tr::phrase<> TitleText(Ui::FilterLinkHeaderType type) {
using Type = Ui::FilterLinkHeaderType;
switch (type) {
case Type::AddingFilter: return tr::lng_filters_by_link_title;
case Type::AddingChats: return tr::lng_filters_by_link_more;
case Type::AllAdded: return tr::lng_filters_by_link_already;
case Type::Removing: return tr::lng_filters_by_link_remove;
}
Unexpected("Ui::FilterLinkHeaderType in TitleText.");
}
[[nodiscard]] TextWithEntities AboutText(
Ui::FilterLinkHeaderType type,
TextWithEntities title) {
using Type = Ui::FilterLinkHeaderType;
auto boldTitle = Ui::Text::Wrapped(title, EntityType::Bold);
return (type == Type::AddingFilter)
? tr::lng_filters_by_link_sure(
tr::now,
lt_folder,
std::move(boldTitle),
tr::marked)
: (type == Type::AddingChats)
? tr::lng_filters_by_link_more_sure(
tr::now,
lt_folder,
std::move(boldTitle),
tr::marked)
: (type == Type::AllAdded)
? tr::lng_filters_by_link_already_about(
tr::now,
lt_folder,
std::move(boldTitle),
tr::marked)
: tr::lng_filters_by_link_remove_sure(
tr::now,
lt_folder,
std::move(boldTitle),
tr::marked);
}
void InitFilterLinkHeader(
not_null<PeerListBox*> box,
Fn<void(int minHeight, int maxHeight, int addedTopHeight)> adjust,
Ui::FilterLinkHeaderType type,
Data::ChatFilterTitle title,
QString iconEmoji,
rpl::producer<int> count,
bool horizontalFilters) {
const auto icon = Ui::LookupFilterIcon(
Ui::LookupFilterIconByEmoji(
iconEmoji
).value_or(Ui::FilterIcon::Custom)).active;
const auto isStatic = title.isStatic;
auto header = Ui::MakeFilterLinkHeader(box, {
.type = type,
.title = TitleText(type)(tr::now),
.about = AboutText(type, title.text),
.aboutContext = Core::TextContext({
.session = &box->peerListUiShow()->session(),
.customEmojiLoopLimit = isStatic ? -1 : 0,
}),
.folderTitle = title.text,
.folderIcon = icon,
.badge = (type == Ui::FilterLinkHeaderType::AddingChats
? std::move(count)
: rpl::single(0)),
.horizontalFilters = horizontalFilters,
});
const auto widget = header.widget;
widget->resizeToWidth(st::boxWideWidth);
Ui::SendPendingMoveResizeEvents(widget);
const auto min = widget->minimumHeight(), max = widget->maximumHeight();
widget->resize(st::boxWideWidth, max);
box->setAddedTopScrollSkip(max);
std::move(
header.wheelEvents
) | rpl::on_next([=](not_null<QWheelEvent*> e) {
box->sendScrollViewportEvent(e);
}, widget->lifetime());
std::move(
header.closeRequests
) | rpl::on_next([=] {
box->closeBox();
}, widget->lifetime());
struct State {
bool processing = false;
int addedTopHeight = 0;
};
const auto state = widget->lifetime().make_state<State>();
box->scrolls(
) | rpl::filter([=] {
return !state->processing;
}) | rpl::on_next([=] {
state->processing = true;
const auto guard = gsl::finally([&] { state->processing = false; });
const auto top = box->scrollTop();
const auto headerHeight = std::max(max - top, min);
const auto addedTopHeight = max - headerHeight;
widget->resize(widget->width(), headerHeight);
if (state->addedTopHeight < addedTopHeight) {
adjust(min, max, addedTopHeight);
box->setAddedTopScrollSkip(headerHeight);
} else {
box->setAddedTopScrollSkip(headerHeight);
adjust(min, max, addedTopHeight);
}
state->addedTopHeight = addedTopHeight;
box->peerListRefreshRows();
}, widget->lifetime());
box->setNoContentMargin(true);
adjust(min, max, 0);
}
void ImportInvite(
const QString &slug,
FilterId filterId,
const base::flat_set<not_null<PeerData*>> &peers,
Fn<void()> done,
Fn<void(QString)> fail) {
Expects(!peers.empty());
const auto peer = peers.front();
const auto api = &peer->session().api();
const auto callback = [=](const MTPUpdates &result) {
api->applyUpdates(result);
if (slug.isEmpty()) {
peer->owner().chatsFilters().moreChatsHide(filterId, true);
}
done();
};
const auto error = [=](const MTP::Error &error) {
fail(error.type());
};
auto inputs = peers | ranges::views::transform([](auto peer) {
return MTPInputPeer(peer->input());
}) | ranges::to<QVector<MTPInputPeer>>();
if (!slug.isEmpty()) {
api->request(MTPchatlists_JoinChatlistInvite(
MTP_string(slug),
MTP_vector<MTPInputPeer>(std::move(inputs))
)).done(callback).fail(error).handleFloodErrors().send();
} else {
api->request(MTPchatlists_JoinChatlistUpdates(
MTP_inputChatlistDialogFilter(MTP_int(filterId)),
MTP_vector<MTPInputPeer>(std::move(inputs))
)).done(callback).fail(error).handleFloodErrors().send();
}
}
ToggleChatsController::ToggleChatsController(
not_null<Window::SessionController*> window,
ToggleAction action,
Data::ChatFilterTitle title,
std::vector<not_null<PeerData*>> chats,
std::vector<not_null<PeerData*>> additional)
: _window(window)
, _action(action)
, _chats(std::move(chats))
, _additional(std::move(additional)) {
setStyleOverrides(&st::filterLinkChatsList);
}
void ToggleChatsController::prepare() {
auto selected = base::flat_set<not_null<PeerData*>>();
const auto disabled = [](not_null<PeerData*> peer) {
return peer->isChat()
? peer->asChat()->isForbidden()
: peer->isChannel()
? peer->asChannel()->isForbidden()
: false;
};
const auto add = [&](not_null<PeerData*> peer, bool additional = false) {
const auto disable = disabled(peer);
auto row = (additional || !disable)
? std::make_unique<PeerListRow>(peer)
: MakeFilterChatRow(
peer,
tr::lng_filters_link_inaccessible(tr::now),
true);
if (delegate()->peerListFindRow(peer->id.value)) {
return;
}
const auto raw = row.get();
delegate()->peerListAppendRow(std::move(row));
if (!disable
&& (!additional || _action == ToggleAction::Removing)) {
_checkable.emplace(peer);
if (const auto status = FilterChatStatusText(peer)
; !status.isEmpty()) {
raw->setCustomStatus(status);
}
}
if (disable) {
} else if (!additional) {
delegate()->peerListSetRowChecked(raw, true);
raw->finishCheckedAnimation();
selected.emplace(peer);
} else if (_action == ToggleAction::Adding) {
raw->setDisabledState(PeerListRow::State::DisabledChecked);
raw->setCustomStatus(peer->isBroadcast()
? tr::lng_filters_link_already_channel(tr::now)
: tr::lng_filters_link_already_group(tr::now));
}
};
for (const auto &peer : _chats) {
if (!disabled(peer)) {
add(peer);
}
}
for (const auto &peer : _additional) {
add(peer, true);
}
for (const auto &peer : _chats) {
if (disabled(peer)) {
add(peer);
}
}
setupAboveWidget();
setupBelowWidget();
initDesiredHeightValue();
delegate()->peerListRefreshRows();
_selected = std::move(selected);
}
void ToggleChatsController::rowClicked(not_null<PeerListRow*> row) {
const auto peer = row->peer();
if (!_checkable.contains(peer)) {
return;
}
const auto checked = row->checked();
auto selected = _selected.current();
delegate()->peerListSetRowChecked(row, !checked);
if (checked) {
selected.remove(peer);
} else {
selected.emplace(peer);
}
_selected = std::move(selected);
}
void ToggleChatsController::setupAboveWidget() {
using namespace Settings;
auto wrap = object_ptr<Ui::VerticalLayout>((QWidget*)nullptr);
const auto container = wrap.data();
_addedTopWidget = container->add(object_ptr<Ui::RpWidget>(container));
const auto realAbove = container->add(
object_ptr<Ui::VerticalLayout>(container));
Ui::AddDivider(realAbove);
const auto totalCount = [&] {
if (_chats.empty()) {
return _additional.size();
} else if (_additional.empty()) {
return _chats.size();
}
auto result = _chats.size();
for (const auto &peer : _additional) {
if (!ranges::contains(_chats, peer)) {
++result;
}
}
return result;
};
const auto count = (_action == ToggleAction::Removing)
? totalCount()
: _chats.empty()
? _additional.size()
: _chats.size();
const auto selectableCount = int(_checkable.size());
auto selectedCount = _selected.value(
) | rpl::map([](const base::flat_set<not_null<PeerData*>> &selected) {
return int(selected.size());
});
AddFilterSubtitleWithToggles(
realAbove,
(_action == ToggleAction::Removing
? tr::lng_filters_by_link_quit
: _chats.empty()
? tr::lng_filters_by_link_in
: tr::lng_filters_by_link_join)(
lt_count,
rpl::single(float64(count))),
selectableCount,
std::move(selectedCount),
[=](bool select) { toggleAllSelected(select); });
_aboveHeight = realAbove->heightValue();
delegate()->peerListSetAboveWidget(std::move(wrap));
}
void ToggleChatsController::toggleAllSelected(bool select) {
auto selected = _selected.current();
if (!select) {
if (selected.empty()) {
return;
}
for (const auto &peer : selected) {
const auto row = delegate()->peerListFindRow(peer->id.value);
Assert(row != nullptr);
delegate()->peerListSetRowChecked(row, false);
}
selected = {};
} else {
const auto count = delegate()->peerListFullRowsCount();
for (auto i = 0; i != count; ++i) {
const auto row = delegate()->peerListRowAt(i);
const auto peer = row->peer();
if (_action != ToggleAction::Adding ||
!ranges::contains(_additional, peer)) {
delegate()->peerListSetRowChecked(row, true);
selected.emplace(peer);
}
}
}
_selected = std::move(selected);
}
void ToggleChatsController::setupBelowWidget() {
if (_chats.empty()) {
auto widget = object_ptr<Ui::RpWidget>((QWidget*)nullptr);
_addedBottomWidget = widget.data();
delegate()->peerListSetBelowWidget(std::move(widget));
return;
}
auto layout = object_ptr<Ui::VerticalLayout>((QWidget*)nullptr);
const auto raw = layout.data();
auto widget = object_ptr<Ui::DividerLabel>(
(QWidget*)nullptr,
std::move(layout),
st::defaultBoxDividerLabelPadding);
raw->add(object_ptr<Ui::FlatLabel>(
raw,
(_action == ToggleAction::Removing
? tr::lng_filters_by_link_about_quit
: tr::lng_filters_by_link_about)(tr::now),
st::boxDividerLabel));
_addedBottomWidget = raw->add(object_ptr<Ui::RpWidget>(raw));
_belowHeight = widget->heightValue() | rpl::map([=](int value) {
return value - _addedBottomWidget->height();
});
delegate()->peerListSetBelowWidget(std::move(widget));
}
Main::Session &ToggleChatsController::session() const {
return _window->session();
}
auto ToggleChatsController::selectedValue() const
-> rpl::producer<base::flat_set<not_null<PeerData*>>> {
return _selected.value();
}
void ToggleChatsController::adjust(
int minHeight,
int maxHeight,
int addedTopHeight) {
Expects(addedTopHeight >= 0);
_addedTopWidget->resize(_addedTopWidget->width(), addedTopHeight);
_minTopHeight = minHeight;
_maxTopHeight = maxHeight;
}
void ToggleChatsController::setRealContentHeight(rpl::producer<int> value) {
std::move(
value
) | rpl::on_next([=](int height) {
const auto desired = _desiredHeight.current();
if (height <= computeListSt().item.height) {
return;
} else if (height >= desired) {
_addedBottomWidget->resize(_addedBottomWidget->width(), 0);
} else {
const auto available = desired - height;
const auto required = _maxTopHeight.current() - _minTopHeight;
const auto added = required - available;
_addedBottomWidget->resize(
_addedBottomWidget->width(),
std::max(added, 0));
}
}, _lifetime);
}
void ToggleChatsController::initDesiredHeightValue() {
using namespace rpl::mappers;
const auto &st = computeListSt();
const auto count = int(delegate()->peerListFullRowsCount());
const auto middle = st.padding.top()
+ (count * st.item.height)
+ st.padding.bottom();
_desiredHeight = rpl::combine(
_maxTopHeight.value(),
_aboveHeight.value(),
_belowHeight.value(),
_1 + _2 + middle + _3);
}
rpl::producer<int> ToggleChatsController::boxHeightValue() const {
return _desiredHeight.value() | rpl::map([=](int value) {
return std::min(value, st::boxMaxListHeight);
});
}
void ShowImportError(
not_null<Window::SessionController*> window,
FilterId id,
int added,
const QString &error) {
const auto session = &window->session();
const auto &list = session->data().chatsFilters().list();
const auto i = ranges::find(list, id, &Data::ChatFilter::id);
const auto count = added
+ ((i != end(list)) ? int(i->always().size()) : 0);
if (error == u"CHANNELS_TOO_MUCH"_q) {
window->show(Box(ChannelsLimitBox, session));
} else if (error == u"FILTER_INCLUDE_TOO_MUCH"_q) {
window->show(Box(FilterChatsLimitBox, session, count, true));
} else if (error == u"CHATLISTS_TOO_MUCH"_q) {
window->show(Box(ShareableFiltersLimitBox, session));
} else {
window->showToast((error == u"INVITE_SLUG_EXPIRED"_q)
? tr::lng_group_invite_bad_link(tr::now)
: error.startsWith(u"FLOOD_WAIT_"_q)
? tr::lng_flood_error(tr::now)
: error);
}
}
void ShowImportToast(
base::weak_ptr<Window::SessionController> weak,
Data::ChatFilterTitle title,
Ui::FilterLinkHeaderType type,
int added) {
const auto strong = weak.get();
if (!strong) {
return;
}
const auto created = (type == Ui::FilterLinkHeaderType::AddingFilter);
const auto phrase = created
? tr::lng_filters_added_title
: tr::lng_filters_updated_title;
auto text = Ui::Text::Wrapped(
phrase(tr::now, lt_folder, title.text, tr::marked),
EntityType::Bold);
if (added > 0) {
const auto phrase = created
? tr::lng_filters_added_also
: tr::lng_filters_updated_also;
text.append('\n').append(phrase(tr::now, lt_count, added));
}
const auto isStatic = title.isStatic;
strong->showToast({
.text = std::move(text),
.textContext = Core::TextContext({
.session = &strong->session(),
.customEmojiLoopLimit = isStatic ? -1 : 0,
})
});
}
void HandleEnterInBox(not_null<Ui::BoxContent*> box) {
const auto isEnter = [=](not_null<QEvent*> event) {
if (event->type() == QEvent::KeyPress) {
if (const auto k = static_cast<QKeyEvent*>(event.get())) {
return (k->key() == Qt::Key_Enter)
|| (k->key() == Qt::Key_Return);
}
}
return false;
};
base::install_event_filter(box, [=](not_null<QEvent*> event) {
if (isEnter(event)) {
box->triggerButton(0);
return base::EventFilterResult::Cancel;
}
return base::EventFilterResult::Continue;
});
}
void ProcessFilterInvite(
base::weak_ptr<Window::SessionController> weak,
const QString &slug,
FilterId filterId,
Data::ChatFilterTitle title,
QString iconEmoji,
std::vector<not_null<PeerData*>> peers,
std::vector<not_null<PeerData*>> already) {
const auto strong = weak.get();
if (!strong) {
return;
}
Core::App().hideMediaView();
if (peers.empty() && !filterId) {
strong->showToast(tr::lng_group_invite_bad_link(tr::now));
return;
}
const auto fullyAdded = (peers.empty() && filterId);
auto controller = std::make_unique<ToggleChatsController>(
strong,
ToggleAction::Adding,
title,
std::move(peers),
std::move(already));
const auto horizontalFilters = !strong->enoughSpaceForFilters()
|| Core::App().settings().chatFiltersHorizontal();
const auto raw = controller.get();
auto initBox = [=](not_null<PeerListBox*> box) {
box->setStyle(st::filterInviteBox);
using Type = Ui::FilterLinkHeaderType;
const auto type = fullyAdded
? Type::AllAdded
: !filterId
? Type::AddingFilter
: Type::AddingChats;
auto badge = raw->selectedValue(
) | rpl::map([=](const base::flat_set<not_null<PeerData*>> &peers) {
return int(peers.size());
});
InitFilterLinkHeader(box, [=](int min, int max, int addedTop) {
raw->adjust(min, max, addedTop);
}, type, title, iconEmoji, rpl::duplicate(badge), horizontalFilters);
raw->setRealContentHeight(box->heightValue());
const auto isStatic = title.isStatic;
auto owned = Ui::FilterLinkProcessButton(
box,
type,
title.text,
Core::TextContext({
.session = &strong->session(),
.customEmojiLoopLimit = isStatic ? -1 : 0,
}),
std::move(badge));
const auto button = owned.data();
box->widthValue(
) | rpl::on_next([=](int width) {
const auto &padding = st::filterInviteBox.buttonPadding;
button->resizeToWidth(width
- padding.left()
- padding.right());
button->moveToLeft(padding.left(), padding.top());
}, button->lifetime());
box->addButton(std::move(owned));
HandleEnterInBox(box);
struct State {
bool importing = false;
};
const auto state = box->lifetime().make_state<State>();
raw->selectedValue(
) | rpl::on_next([=](
base::flat_set<not_null<PeerData*>> &&peers) {
button->setClickedCallback([=] {
if (peers.empty()) {
box->closeBox();
} else if (!state->importing) {
state->importing = true;
const auto added = int(peers.size());
ImportInvite(slug, filterId, peers, crl::guard(box, [=] {
ShowImportToast(weak, title, type, peers.size());
box->closeBox();
}), crl::guard(box, [=](QString text) {
if (const auto strong = weak.get()) {
ShowImportError(strong, filterId, added, text);
}
state->importing = false;
}));
}
});
}, box->lifetime());
};
strong->show(
Box<PeerListBox>(std::move(controller), std::move(initBox)));
}
void ProcessFilterInvite(
base::weak_ptr<Window::SessionController> weak,
const QString &slug,
FilterId filterId,
std::vector<not_null<PeerData*>> peers,
std::vector<not_null<PeerData*>> already) {
const auto strong = weak.get();
if (!strong) {
return;
}
Core::App().hideMediaView();
const auto &list = strong->session().data().chatsFilters().list();
const auto it = ranges::find(list, filterId, &Data::ChatFilter::id);
if (it == end(list)) {
strong->showToast(u"Filter not found :shrug:"_q);
return;
}
ProcessFilterInvite(
weak,
slug,
filterId,
it->title(),
it->iconEmoji(),
std::move(peers),
std::move(already));
}
} // namespace
void SaveNewFilterPinned(
not_null<Main::Session*> session,
FilterId filterId) {
const auto &order = session->data().pinnedChatsOrder(filterId);
auto &filters = session->data().chatsFilters();
const auto &filter = filters.applyUpdatedPinned(filterId, order);
session->api().request(MTPmessages_UpdateDialogFilter(
MTP_flags(MTPmessages_UpdateDialogFilter::Flag::f_filter),
MTP_int(filterId),
filter.tl()
)).send();
}
void CheckFilterInvite(
not_null<Window::SessionController*> controller,
const QString &slug) {
const auto session = &controller->session();
const auto weak = base::make_weak(controller);
session->api().checkFilterInvite(slug, [=](
const MTPchatlists_ChatlistInvite &result) {
const auto strong = weak.get();
if (!strong) {
return;
}
auto title = Data::ChatFilterTitle();
auto iconEmoji = QString();
auto filterId = FilterId();
auto peers = std::vector<not_null<PeerData*>>();
auto already = std::vector<not_null<PeerData*>>();
auto &owner = strong->session().data();
result.match([&](const auto &data) {
owner.processUsers(data.vusers());
owner.processChats(data.vchats());
});
const auto parseList = [&](const MTPVector<MTPPeer> &list) {
auto result = std::vector<not_null<PeerData*>>();
result.reserve(list.v.size());
for (const auto &peer : list.v) {
result.push_back(owner.peer(peerFromMTP(peer)));
}
return result;
};
result.match([&](const MTPDchatlists_chatlistInvite &data) {
title.text = ParseTextWithEntities(session, data.vtitle());
title.isStatic = data.is_title_noanimate();
iconEmoji = data.vemoticon().value_or_empty();
peers = parseList(data.vpeers());
}, [&](const MTPDchatlists_chatlistInviteAlready &data) {
filterId = data.vfilter_id().v;
peers = parseList(data.vmissing_peers());
already = parseList(data.valready_peers());
});
const auto notLoaded = filterId
&& !ranges::contains(
owner.chatsFilters().list(),
filterId,
&Data::ChatFilter::id);
if (notLoaded) {
const auto lifetime = std::make_shared<rpl::lifetime>();
owner.chatsFilters().changed(
) | rpl::on_next([=] {
lifetime->destroy();
ProcessFilterInvite(
weak,
slug,
filterId,
std::move(peers),
std::move(already));
}, *lifetime);
owner.chatsFilters().reload();
} else if (filterId) {
ProcessFilterInvite(
weak,
slug,
filterId,
std::move(peers),
std::move(already));
} else {
ProcessFilterInvite(
weak,
slug,
filterId,
title,
iconEmoji,
std::move(peers),
std::move(already));
}
}, [=](const MTP::Error &error) {
if (error.code() != 400) {
return;
}
ProcessFilterInvite(weak, slug, {}, {}, {}, {}, {});
});
}
void ProcessFilterUpdate(
base::weak_ptr<Window::SessionController> weak,
FilterId filterId,
std::vector<not_null<PeerData*>> missing) {
if (const auto strong = missing.empty() ? weak.get() : nullptr) {
strong->session().data().chatsFilters().moreChatsHide(filterId);
return;
}
ProcessFilterInvite(weak, QString(), filterId, std::move(missing), {});
}
void ProcessFilterRemove(
base::weak_ptr<Window::SessionController> weak,
Data::ChatFilterTitle title,
QString iconEmoji,
std::vector<not_null<PeerData*>> all,
std::vector<not_null<PeerData*>> suggest,
Fn<void(std::vector<not_null<PeerData*>>)> done) {
const auto strong = weak.get();
if (!strong) {
return;
}
Core::App().hideMediaView();
if (all.empty() && suggest.empty()) {
done({});
return;
}
auto controller = std::make_unique<ToggleChatsController>(
strong,
ToggleAction::Removing,
title,
std::move(suggest),
std::move(all));
const auto horizontalFilters = !strong->enoughSpaceForFilters()
|| Core::App().settings().chatFiltersHorizontal();
const auto raw = controller.get();
auto initBox = [=](not_null<PeerListBox*> box) {
box->setStyle(st::filterInviteBox);
const auto type = Ui::FilterLinkHeaderType::Removing;
auto badge = raw->selectedValue(
) | rpl::map([=](const base::flat_set<not_null<PeerData*>> &peers) {
return int(peers.size());
});
InitFilterLinkHeader(box, [=](int min, int max, int addedTop) {
raw->adjust(min, max, addedTop);
}, type, title, iconEmoji, rpl::single(0), horizontalFilters);
const auto isStatic = title.isStatic;
auto owned = Ui::FilterLinkProcessButton(
box,
type,
title.text,
Core::TextContext({
.session = &strong->session(),
.customEmojiLoopLimit = isStatic ? -1 : 0,
}),
std::move(badge));
const auto button = owned.data();
box->widthValue(
) | rpl::on_next([=](int width) {
const auto &padding = st::filterInviteBox.buttonPadding;
button->resizeToWidth(width
- padding.left()
- padding.right());
button->moveToLeft(padding.left(), padding.top());
}, button->lifetime());
box->addButton(std::move(owned));
HandleEnterInBox(box);
raw->selectedValue(
) | rpl::on_next([=](
base::flat_set<not_null<PeerData*>> &&peers) {
button->setClickedCallback([=] {
done(peers | ranges::to_vector);
box->closeBox();
});
}, box->lifetime());
};
strong->show(
Box<PeerListBox>(std::move(controller), std::move(initBox)));
}
[[nodiscard]] std::vector<not_null<PeerData*>> ExtractSuggestRemoving(
const Data::ChatFilter &filter) {
if (!filter.chatlist()) {
return {};
}
return filter.always() | ranges::views::filter([](
not_null<History*> history) {
return history->peer->isChannel();
}) | ranges::views::transform(&History::peer) | ranges::to_vector;
}
} // namespace Api

View File

@@ -0,0 +1,49 @@
/*
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
*/
#pragma once
namespace Main {
class Session;
} // namespace Main
namespace Window {
class SessionController;
} // namespace Window
namespace Data {
class ChatFilter;
struct ChatFilterTitle;
} // namespace Data
namespace Api {
void SaveNewFilterPinned(
not_null<Main::Session*> session,
FilterId filterId);
void CheckFilterInvite(
not_null<Window::SessionController*> controller,
const QString &slug);
void ProcessFilterUpdate(
base::weak_ptr<Window::SessionController> weak,
FilterId filterId,
std::vector<not_null<PeerData*>> missing);
void ProcessFilterRemove(
base::weak_ptr<Window::SessionController> weak,
Data::ChatFilterTitle title,
QString iconEmoji,
std::vector<not_null<PeerData*>> all,
std::vector<not_null<PeerData*>> suggest,
Fn<void(std::vector<not_null<PeerData*>>)> done);
[[nodiscard]] std::vector<not_null<PeerData*>> ExtractSuggestRemoving(
const Data::ChatFilter &filter);
} // namespace Api

View File

@@ -0,0 +1,128 @@
/*
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 "api/api_chat_filters_remove_manager.h"
#include "api/api_chat_filters.h"
#include "apiwrap.h"
#include "data/data_chat_filters.h"
#include "data/data_peer.h"
#include "data/data_session.h"
#include "lang/lang_keys.h"
#include "main/main_session.h"
#include "ui/boxes/confirm_box.h"
#include "ui/ui_utility.h"
#include "window/window_controller.h"
#include "window/window_session_controller.h"
#include "styles/style_layers.h"
namespace Api {
namespace {
void RemoveChatFilter(
not_null<Main::Session*> session,
FilterId filterId,
std::vector<not_null<PeerData*>> leave) {
const auto api = &session->api();
session->data().chatsFilters().apply(MTP_updateDialogFilter(
MTP_flags(MTPDupdateDialogFilter::Flag(0)),
MTP_int(filterId),
MTPDialogFilter()));
if (leave.empty()) {
api->request(MTPmessages_UpdateDialogFilter(
MTP_flags(MTPmessages_UpdateDialogFilter::Flag(0)),
MTP_int(filterId),
MTPDialogFilter()
)).send();
} else {
api->request(MTPchatlists_LeaveChatlist(
MTP_inputChatlistDialogFilter(MTP_int(filterId)),
MTP_vector<MTPInputPeer>(ranges::views::all(
leave
) | ranges::views::transform([](not_null<PeerData*> peer) {
return MTPInputPeer(peer->input());
}) | ranges::to<QVector<MTPInputPeer>>())
)).done([=](const MTPUpdates &result) {
api->applyUpdates(result);
}).send();
}
}
} // namespace
RemoveComplexChatFilter::RemoveComplexChatFilter() = default;
void RemoveComplexChatFilter::request(
base::weak_qptr<Ui::RpWidget> widget,
base::weak_ptr<Window::SessionController> weak,
FilterId id) {
const auto session = &weak->session();
const auto &list = session->data().chatsFilters().list();
const auto i = ranges::find(list, id, &Data::ChatFilter::id);
const auto filter = (i != end(list)) ? *i : Data::ChatFilter();
const auto has = filter.hasMyLinks();
const auto confirm = [=](Fn<void()> action, bool onlyWhenHas = false) {
if (!has && onlyWhenHas) {
action();
return;
}
weak->window().show(Ui::MakeConfirmBox({
.text = (has
? tr::lng_filters_delete_sure()
: tr::lng_filters_remove_sure()),
.confirmed = [=](Fn<void()> &&close) { close(); action(); },
.confirmText = (has
? tr::lng_box_delete()
: tr::lng_filters_remove_yes()),
.confirmStyle = &st::attentionBoxButton,
}));
};
const auto simple = [=] {
confirm([=] { RemoveChatFilter(session, id, {}); });
};
const auto suggestRemoving = Api::ExtractSuggestRemoving(filter);
if (suggestRemoving.empty()) {
simple();
return;
} else if (_removingRequestId) {
if (_removingId == id) {
return;
}
session->api().request(_removingRequestId).cancel();
}
_removingId = id;
_removingRequestId = session->api().request(
MTPchatlists_GetLeaveChatlistSuggestions(
MTP_inputChatlistDialogFilter(
MTP_int(id)))
).done(crl::guard(widget, [=, this](const MTPVector<MTPPeer> &result) {
_removingRequestId = 0;
const auto suggestRemovePeers = ranges::views::all(
result.v
) | ranges::views::transform([=](const MTPPeer &peer) {
return session->data().peer(peerFromMTP(peer));
}) | ranges::to_vector;
const auto chosen = crl::guard(widget, [=](
std::vector<not_null<PeerData*>> peers) {
RemoveChatFilter(session, id, std::move(peers));
});
confirm(crl::guard(widget, [=] {
Api::ProcessFilterRemove(
weak,
filter.title(),
filter.iconEmoji(),
suggestRemoving,
suggestRemovePeers,
chosen);
}), true);
})).fail(crl::guard(widget, [=, this] {
_removingRequestId = 0;
simple();
})).send();
}
} // namespace Api

View File

@@ -0,0 +1,35 @@
/*
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
*/
#pragma once
namespace Window {
class SessionController;
} // namespace Window
namespace Ui {
class RpWidget;
} // namespace Ui
namespace Api {
class RemoveComplexChatFilter final {
public:
RemoveComplexChatFilter();
void request(
base::weak_qptr<Ui::RpWidget> widget,
base::weak_ptr<Window::SessionController> weak,
FilterId id);
private:
FilterId _removingId = 0;
mtpRequestId _removingRequestId = 0;
};
} // namespace Api

View File

@@ -0,0 +1,704 @@
/*
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 "api/api_chat_invite.h"
#include "apiwrap.h"
#include "api/api_credits.h"
#include "boxes/premium_limits_box.h"
#include "core/application.h"
#include "data/components/credits.h"
#include "data/data_channel.h"
#include "data/data_file_origin.h"
#include "data/data_forum.h"
#include "data/data_photo.h"
#include "data/data_photo_media.h"
#include "data/data_session.h"
#include "data/data_user.h"
#include "info/channel_statistics/boosts/giveaway/boost_badge.h"
#include "info/profile/info_profile_badge.h"
#include "lang/lang_keys.h"
#include "main/main_session.h"
#include "settings/settings_credits_graphics.h"
#include "ui/boxes/confirm_box.h"
#include "ui/controls/userpic_button.h"
#include "ui/effects/credits_graphics.h"
#include "ui/effects/premium_graphics.h"
#include "ui/effects/premium_stars_colored.h"
#include "ui/empty_userpic.h"
#include "ui/layers/generic_box.h"
#include "ui/painter.h"
#include "ui/rect.h"
#include "ui/text/text_utilities.h"
#include "ui/toast/toast.h"
#include "ui/vertical_list.h"
#include "window/window_session_controller.h"
#include "styles/style_boxes.h"
#include "styles/style_chat_helpers.h"
#include "styles/style_credits.h"
#include "styles/style_info.h"
#include "styles/style_layers.h"
#include "styles/style_premium.h"
namespace Api {
namespace {
void SubmitChatInvite(
base::weak_ptr<Window::SessionController> weak,
not_null<Main::Session*> session,
const QString &hash,
bool isGroup) {
session->api().request(MTPmessages_ImportChatInvite(
MTP_string(hash)
)).done([=](const MTPUpdates &result) {
session->api().applyUpdates(result);
const auto strongController = weak.get();
if (!strongController) {
return;
}
strongController->hideLayer();
const auto handleChats = [&](const MTPVector<MTPChat> &chats) {
if (chats.v.isEmpty()) {
return;
}
const auto peerId = chats.v[0].match([](const MTPDchat &data) {
return peerFromChat(data.vid().v);
}, [](const MTPDchannel &data) {
return peerFromChannel(data.vid().v);
}, [](auto&&) {
return PeerId(0);
});
if (const auto peer = session->data().peerLoaded(peerId)) {
// Shows in the primary window anyway.
strongController->showPeerHistory(
peer,
Window::SectionShow::Way::Forward);
}
};
result.match([&](const MTPDupdates &data) {
handleChats(data.vchats());
}, [&](const MTPDupdatesCombined &data) {
handleChats(data.vchats());
}, [&](auto &&) {
LOG(("API Error: unexpected update cons %1 "
"(ApiWrap::importChatInvite)").arg(result.type()));
});
}).fail([=](const MTP::Error &error) {
const auto &type = error.type();
const auto strongController = weak.get();
if (!strongController) {
return;
} else if (type == u"CHANNELS_TOO_MUCH"_q) {
strongController->show(
Box(ChannelsLimitBox, &strongController->session()));
return;
}
strongController->hideLayer();
strongController->showToast([&] {
if (type == u"INVITE_REQUEST_SENT"_q) {
return isGroup
? tr::lng_group_request_sent(tr::now)
: tr::lng_group_request_sent_channel(tr::now);
} else if (type == u"USERS_TOO_MUCH"_q) {
return tr::lng_group_invite_no_room(tr::now);
} else {
return tr::lng_group_invite_bad_link(tr::now);
}
}(), ApiWrap::kJoinErrorDuration);
}).send();
}
void ConfirmSubscriptionBox(
not_null<Ui::GenericBox*> box,
not_null<Main::Session*> session,
const QString &hash,
const MTPDchatInvite *data) {
box->setWidth(st::boxWideWidth);
const auto amount = data->vsubscription_pricing()->data().vamount().v;
const auto formId = data->vsubscription_form_id()->v;
const auto name = qs(data->vtitle());
const auto maybePhoto = session->data().processPhoto(data->vphoto());
const auto photo = maybePhoto->isNull() ? nullptr : maybePhoto.get();
struct State final {
std::shared_ptr<Data::PhotoMedia> photoMedia;
std::unique_ptr<Ui::EmptyUserpic> photoEmpty;
QImage frame;
std::optional<MTP::Sender> api;
Ui::RpWidget* saveButton = nullptr;
rpl::variable<bool> loading;
};
const auto state = box->lifetime().make_state<State>();
const auto content = box->verticalLayout();
Ui::AddSkip(content, st::confirmInvitePhotoTop);
const auto userpic = content->add(
object_ptr<Ui::RpWidget>(content),
style::al_top);
const auto photoSize = st::confirmInvitePhotoSize;
userpic->resize(Size(photoSize));
userpic->setNaturalWidth(photoSize);
const auto creditsIconSize = photoSize / 3;
const auto creditsIconCallback =
Ui::PaintOutlinedColoredCreditsIconCallback(
creditsIconSize,
1.5);
state->frame = QImage(
Size(photoSize * style::DevicePixelRatio()),
QImage::Format_ARGB32_Premultiplied);
state->frame.setDevicePixelRatio(style::DevicePixelRatio());
const auto options = Images::Option::RoundCircle;
userpic->paintRequest(
) | rpl::on_next([=, small = Data::PhotoSize::Small] {
state->frame.fill(Qt::transparent);
{
auto p = QPainter(&state->frame);
if (state->photoMedia) {
if (const auto image = state->photoMedia->image(small)) {
p.drawPixmap(
0,
0,
image->pix(Size(photoSize), { .options = options }));
}
} else if (state->photoEmpty) {
state->photoEmpty->paintCircle(
p,
0,
0,
userpic->width(),
photoSize);
}
if (creditsIconCallback) {
p.translate(
photoSize - creditsIconSize,
photoSize - creditsIconSize);
creditsIconCallback(p);
}
}
auto p = QPainter(userpic);
p.drawImage(0, 0, state->frame);
}, userpic->lifetime());
userpic->setAttribute(Qt::WA_TransparentForMouseEvents);
if (photo) {
state->photoMedia = photo->createMediaView();
state->photoMedia->wanted(Data::PhotoSize::Small, Data::FileOrigin());
if (!state->photoMedia->image(Data::PhotoSize::Small)) {
session->downloaderTaskFinished(
) | rpl::on_next([=] {
userpic->update();
}, userpic->lifetime());
}
} else {
state->photoEmpty = std::make_unique<Ui::EmptyUserpic>(
Ui::EmptyUserpic::UserpicColor(0),
name);
}
Ui::AddSkip(content);
Ui::AddSkip(content);
Settings::AddMiniStars(
content,
Ui::CreateChild<Ui::RpWidget>(content),
photoSize,
box->width(),
2.);
box->addRow(
object_ptr<Ui::FlatLabel>(
box,
tr::lng_channel_invite_subscription_title(),
st::inviteLinkSubscribeBoxTitle),
style::al_top);
box->addRow(
object_ptr<Ui::FlatLabel>(
box,
tr::lng_channel_invite_subscription_about(
lt_channel,
rpl::single(tr::bold(name)),
lt_price,
tr::lng_credits_summary_options_credits(
lt_count,
rpl::single(amount) | tr::to_count(),
tr::bold),
tr::marked),
st::inviteLinkSubscribeBoxAbout),
style::al_top);
Ui::AddSkip(content);
box->addRow(
object_ptr<Ui::FlatLabel>(
box,
tr::lng_channel_invite_subscription_terms(
lt_link,
rpl::combine(
tr::lng_paid_react_agree_link(),
tr::lng_group_invite_subscription_about_url()
) | rpl::map([](const QString &text, const QString &url) {
return tr::link(text, url);
}),
tr::rich),
st::inviteLinkSubscribeBoxTerms),
style::al_top);
{
const auto balance = Settings::AddBalanceWidget(
content,
session,
session->credits().balanceValue(),
true);
session->credits().load(true);
rpl::combine(
balance->sizeValue(),
content->sizeValue()
) | rpl::on_next([=](const QSize &, const QSize &) {
balance->moveToRight(
st::creditsHistoryRightSkip * 2,
st::creditsHistoryRightSkip);
balance->update();
}, balance->lifetime());
}
const auto sendCredits = [=, weak = base::make_weak(box)] {
const auto show = box->uiShow();
const auto buttonWidth = state->saveButton
? state->saveButton->width()
: 0;
const auto finish = [=] {
state->api = std::nullopt;
state->loading.force_assign(false);
if (const auto strong = weak.get()) {
strong->closeBox();
}
};
state->api->request(
MTPpayments_SendStarsForm(
MTP_long(formId),
MTP_inputInvoiceChatInviteSubscription(MTP_string(hash)))
).done([=](const MTPpayments_PaymentResult &result) {
result.match([&](const MTPDpayments_paymentResult &data) {
session->api().applyUpdates(data.vupdates());
}, [](const MTPDpayments_paymentVerificationNeeded &data) {
});
const auto refill = session->data().activeCreditsSubsRebuilder();
const auto strong = weak.get();
if (!strong) {
return;
}
if (!refill) {
return finish();
}
const auto api
= strong->lifetime().make_state<Api::CreditsHistory>(
session->user(),
true,
true);
api->requestSubscriptions({}, [=](Data::CreditsStatusSlice d) {
refill->fire(std::move(d));
finish();
});
}).fail([=](const MTP::Error &error) {
const auto id = error.type();
if (weak) {
state->api = std::nullopt;
}
show->showToast(id);
state->loading.force_assign(false);
}).send();
if (state->saveButton) {
state->saveButton->resizeToWidth(buttonWidth);
}
};
auto confirmText = tr::lng_channel_invite_subscription_button();
state->saveButton = box->addButton(std::move(confirmText), [=] {
if (state->api) {
return;
}
state->api.emplace(&session->mtp());
state->loading.force_assign(true);
const auto done = [=](Settings::SmallBalanceResult result) {
if (result == Settings::SmallBalanceResult::Success
|| result == Settings::SmallBalanceResult::Already) {
sendCredits();
} else {
state->api = std::nullopt;
state->loading.force_assign(false);
}
};
Settings::MaybeRequestBalanceIncrease(
Main::MakeSessionShow(box->uiShow(), session),
amount,
Settings::SmallBalanceSubscription{ .name = name },
done);
});
if (const auto saveButton = state->saveButton) {
using namespace Info::Statistics;
const auto loadingAnimation = InfiniteRadialAnimationWidget(
saveButton,
saveButton->height() / 2,
&st::editStickerSetNameLoading);
AddChildToWidgetCenter(saveButton, loadingAnimation);
loadingAnimation->showOn(
state->loading.value() | rpl::map(rpl::mappers::_1));
}
box->addButton(tr::lng_cancel(), [=] { box->closeBox(); });
}
} // namespace
void CheckChatInvite(
not_null<Window::SessionController*> controller,
const QString &hash,
ChannelData *invitePeekChannel,
Fn<void()> loaded) {
const auto session = &controller->session();
const auto weak = base::make_weak(controller);
session->api().checkChatInvite(hash, [=](const MTPChatInvite &result) {
const auto strong = weak.get();
if (!strong) {
return;
}
if (loaded) {
loaded();
}
Core::App().hideMediaView();
const auto show = [&](not_null<PeerData*> chat) {
const auto way = Window::SectionShow::Way::Forward;
if (const auto forum = chat->forum()) {
strong->showForum(forum, way);
} else {
strong->showPeerHistory(chat, way);
}
};
result.match([=](const MTPDchatInvite &data) {
const auto isGroup = !data.is_broadcast();
const auto hasPricing = !!data.vsubscription_pricing();
const auto canRefulfill = data.is_can_refulfill_subscription();
if (hasPricing
&& !canRefulfill
&& !data.vsubscription_form_id()) {
strong->uiShow()->showToast(
tr::lng_confirm_phone_link_invalid(tr::now));
return;
}
const auto box = (hasPricing && !canRefulfill)
? strong->show(Box(
ConfirmSubscriptionBox,
session,
hash,
&data))
: strong->show(Box<ConfirmInviteBox>(
session,
data,
invitePeekChannel,
[=] { SubmitChatInvite(weak, session, hash, isGroup); }));
if (invitePeekChannel) {
box->boxClosing(
) | rpl::filter([=] {
return !invitePeekChannel->amIn();
}) | rpl::on_next([=] {
if (const auto strong = weak.get()) {
strong->clearSectionStack(Window::SectionShow(
Window::SectionShow::Way::ClearStack,
anim::type::normal,
anim::activation::background));
}
}, box->lifetime());
}
}, [=](const MTPDchatInviteAlready &data) {
if (const auto chat = session->data().processChat(data.vchat())) {
if (const auto channel = chat->asChannel()) {
channel->clearInvitePeek();
}
show(chat);
}
}, [=](const MTPDchatInvitePeek &data) {
if (const auto chat = session->data().processChat(data.vchat())) {
if (const auto channel = chat->asChannel()) {
channel->setInvitePeek(hash, data.vexpires().v);
show(chat);
}
}
});
}, [=](const MTP::Error &error) {
if (MTP::IsFloodError(error)) {
if (const auto strong = weak.get()) {
strong->show(Ui::MakeInformBox(tr::lng_flood_error()));
}
return;
}
if (error.code() != 400) {
return;
}
Core::App().hideMediaView();
if (const auto strong = weak.get()) {
strong->show(Ui::MakeInformBox(tr::lng_group_invite_bad_link()));
}
});
}
} // namespace Api
struct ConfirmInviteBox::Participant {
not_null<UserData*> user;
Ui::PeerUserpicView userpic;
};
ConfirmInviteBox::ConfirmInviteBox(
QWidget*,
not_null<Main::Session*> session,
const MTPDchatInvite &data,
ChannelData *invitePeekChannel,
Fn<void()> submit)
: ConfirmInviteBox(
session,
Parse(session, data),
invitePeekChannel,
std::move(submit)) {
}
ConfirmInviteBox::ConfirmInviteBox(
not_null<Main::Session*> session,
ChatInvite &&invite,
ChannelData *invitePeekChannel,
Fn<void()> submit)
: _session(session)
, _submit(std::move(submit))
, _title(this, st::confirmInviteTitle)
, _badge(std::make_unique<Info::Profile::Badge>(
this,
st::infoPeerBadge,
_session,
rpl::single(Info::Profile::Badge::Content{ BadgeForInvite(invite) }),
nullptr,
[=] { return false; }))
, _status(this, st::confirmInviteStatus)
, _about(this, st::confirmInviteAbout)
, _aboutRequests(this, st::confirmInviteStatus)
, _participants(std::move(invite.participants))
, _isChannel(invite.isChannel && !invite.isMegagroup)
, _requestApprove(invite.isRequestNeeded) {
const auto count = invite.participantsCount;
const auto status = [&] {
return invitePeekChannel
? tr::lng_channel_invite_private(tr::now)
: (!_participants.empty() && _participants.size() < count)
? tr::lng_group_invite_members(tr::now, lt_count, count)
: (count > 0 && _isChannel)
? tr::lng_chat_status_subscribers(
tr::now,
lt_count_decimal,
count)
: (count > 0)
? tr::lng_chat_status_members(tr::now, lt_count_decimal, count)
: _isChannel
? tr::lng_channel_status(tr::now)
: tr::lng_group_status(tr::now);
}();
_title->setText(invite.title);
_status->setText(status);
if (!invite.about.isEmpty()) {
_about->setText(invite.about);
} else {
_about.destroy();
}
if (_requestApprove) {
_aboutRequests->setText(_isChannel
? tr::lng_group_request_about_channel(tr::now)
: tr::lng_group_request_about(tr::now));
} else {
_aboutRequests.destroy();
}
if (invite.photo) {
_photo = invite.photo->createMediaView();
_photo->wanted(Data::PhotoSize::Small, Data::FileOrigin());
if (!_photo->image(Data::PhotoSize::Small)) {
_session->downloaderTaskFinished(
) | rpl::on_next([=] {
update();
}, lifetime());
}
} else {
_photoEmpty = std::make_unique<Ui::EmptyUserpic>(
Ui::EmptyUserpic::UserpicColor(0),
invite.title);
}
}
ConfirmInviteBox::~ConfirmInviteBox() = default;
ConfirmInviteBox::ChatInvite ConfirmInviteBox::Parse(
not_null<Main::Session*> session,
const MTPDchatInvite &data) {
auto participants = std::vector<Participant>();
if (const auto list = data.vparticipants()) {
participants.reserve(list->v.size());
for (const auto &participant : list->v) {
if (const auto user = session->data().processUser(participant)) {
participants.push_back(Participant{ user });
}
}
}
const auto photo = session->data().processPhoto(data.vphoto());
return {
.title = qs(data.vtitle()),
.about = data.vabout().value_or_empty(),
.photo = (photo->isNull() ? nullptr : photo.get()),
.participantsCount = data.vparticipants_count().v,
.participants = std::move(participants),
.isPublic = data.is_public(),
.isChannel = data.is_channel(),
.isMegagroup = data.is_megagroup(),
.isBroadcast = data.is_broadcast(),
.isRequestNeeded = data.is_request_needed(),
.isFake = data.is_fake(),
.isScam = data.is_scam(),
.isVerified = data.is_verified(),
};
}
[[nodiscard]] Info::Profile::BadgeType ConfirmInviteBox::BadgeForInvite(
const ChatInvite &invite) {
using Type = Info::Profile::BadgeType;
return invite.isVerified
? Type::Verified
: invite.isScam
? Type::Scam
: invite.isFake
? Type::Fake
: Type::None;
}
void ConfirmInviteBox::prepare() {
addButton(
(_requestApprove
? tr::lng_group_request_to_join()
: _isChannel
? tr::lng_profile_join_channel()
: tr::lng_profile_join_group()),
_submit);
addButton(tr::lng_cancel(), [=] { closeBox(); });
while (_participants.size() > 4) {
_participants.pop_back();
}
auto newHeight = st::confirmInviteStatusTop + _status->height() + st::boxPadding.bottom();
if (!_participants.empty()) {
int skip = (st::confirmInviteUsersWidth - 4 * st::confirmInviteUserPhotoSize) / 5;
int padding = skip / 2;
_userWidth = (st::confirmInviteUserPhotoSize + 2 * padding);
int sumWidth = _participants.size() * _userWidth;
int left = (st::boxWideWidth - sumWidth) / 2;
for (const auto &participant : _participants) {
auto name = new Ui::FlatLabel(this, st::confirmInviteUserName);
name->resizeToWidth(st::confirmInviteUserPhotoSize + padding);
name->setText(participant.user->firstName.isEmpty()
? participant.user->name()
: participant.user->firstName);
name->moveToLeft(left + (padding / 2), st::confirmInviteUserNameTop);
left += _userWidth;
}
newHeight += st::confirmInviteUserHeight;
}
if (_about) {
const auto padding = st::confirmInviteAboutPadding;
_about->resizeToWidth(st::boxWideWidth - padding.left() - padding.right());
newHeight += padding.top() + _about->height() + padding.bottom();
}
if (_aboutRequests) {
const auto padding = st::confirmInviteAboutRequestsPadding;
_aboutRequests->resizeToWidth(st::boxWideWidth - padding.left() - padding.right());
newHeight += padding.top() + _aboutRequests->height() + padding.bottom();
}
setDimensions(st::boxWideWidth, newHeight);
}
void ConfirmInviteBox::resizeEvent(QResizeEvent *e) {
BoxContent::resizeEvent(e);
const auto padding = st::boxRowPadding;
auto nameWidth = width() - padding.left() - padding.right();
auto badgeWidth = 0;
if (const auto widget = _badge->widget()) {
badgeWidth = st::infoVerifiedCheckPosition.x() + widget->width();
nameWidth -= badgeWidth;
}
_title->resizeToWidth(std::min(nameWidth, _title->textMaxWidth()));
_title->moveToLeft(
(width() - _title->width() - badgeWidth) / 2,
st::confirmInviteTitleTop);
const auto badgeLeft = _title->x() + _title->width();
const auto badgeTop = _title->y();
const auto badgeBottom = _title->y() + _title->height();
_badge->move(badgeLeft, badgeTop, badgeBottom);
_status->move(
(width() - _status->width()) / 2,
st::confirmInviteStatusTop);
auto bottom = _status->y()
+ _status->height()
+ st::boxPadding.bottom()
+ (_participants.empty() ? 0 : st::confirmInviteUserHeight);
if (_about) {
const auto padding = st::confirmInviteAboutPadding;
_about->move((width() - _about->width()) / 2, bottom + padding.top());
bottom += padding.top() + _about->height() + padding.bottom();
}
if (_aboutRequests) {
const auto padding = st::confirmInviteAboutRequestsPadding;
_aboutRequests->move((width() - _aboutRequests->width()) / 2, bottom + padding.top());
}
}
void ConfirmInviteBox::paintEvent(QPaintEvent *e) {
BoxContent::paintEvent(e);
Painter p(this);
if (_photo) {
if (const auto image = _photo->image(Data::PhotoSize::Small)) {
const auto size = st::confirmInvitePhotoSize;
p.drawPixmap(
(width() - size) / 2,
st::confirmInvitePhotoTop,
image->pix(
{ size, size },
{ .options = Images::Option::RoundCircle }));
}
} else if (_photoEmpty) {
_photoEmpty->paintCircle(
p,
(width() - st::confirmInvitePhotoSize) / 2,
st::confirmInvitePhotoTop,
width(),
st::confirmInvitePhotoSize);
}
int sumWidth = _participants.size() * _userWidth;
int left = (width() - sumWidth) / 2;
for (auto &participant : _participants) {
participant.user->paintUserpicLeft(
p,
participant.userpic,
left + (_userWidth - st::confirmInviteUserPhotoSize) / 2,
st::confirmInviteUserPhotoTop,
width(),
st::confirmInviteUserPhotoSize);
left += _userWidth;
}
}

View File

@@ -0,0 +1,108 @@
/*
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
*/
#pragma once
#include "ui/layers/box_content.h"
class UserData;
class ChannelData;
namespace Info::Profile {
class Badge;
enum class BadgeType : uchar;
} // namespace Info::Profile
namespace Main {
class Session;
} // namespace Main
namespace Window {
class SessionController;
} // namespace Window
namespace Data {
class PhotoMedia;
} // namespace Data
namespace Ui {
class EmptyUserpic;
} // namespace Ui
namespace Api {
void CheckChatInvite(
not_null<Window::SessionController*> controller,
const QString &hash,
ChannelData *invitePeekChannel = nullptr,
Fn<void()> loaded = nullptr);
} // namespace Api
class ConfirmInviteBox final : public Ui::BoxContent {
public:
ConfirmInviteBox(
QWidget*,
not_null<Main::Session*> session,
const MTPDchatInvite &data,
ChannelData *invitePeekChannel,
Fn<void()> submit);
~ConfirmInviteBox();
protected:
void prepare() override;
void resizeEvent(QResizeEvent *e) override;
void paintEvent(QPaintEvent *e) override;
private:
struct Participant;
struct ChatInvite {
QString title;
QString about;
PhotoData *photo = nullptr;
int participantsCount = 0;
std::vector<Participant> participants;
bool isPublic = false;
bool isChannel = false;
bool isMegagroup = false;
bool isBroadcast = false;
bool isRequestNeeded = false;
bool isFake = false;
bool isScam = false;
bool isVerified = false;
};
[[nodiscard]] static ChatInvite Parse(
not_null<Main::Session*> session,
const MTPDchatInvite &data);
[[nodiscard]] Info::Profile::BadgeType BadgeForInvite(
const ChatInvite &invite);
ConfirmInviteBox(
not_null<Main::Session*> session,
ChatInvite &&invite,
ChannelData *invitePeekChannel,
Fn<void()> submit);
const not_null<Main::Session*> _session;
Fn<void()> _submit;
object_ptr<Ui::FlatLabel> _title;
std::unique_ptr<Info::Profile::Badge> _badge;
object_ptr<Ui::FlatLabel> _status;
object_ptr<Ui::FlatLabel> _about;
object_ptr<Ui::FlatLabel> _aboutRequests;
std::shared_ptr<Data::PhotoMedia> _photo;
std::unique_ptr<Ui::EmptyUserpic> _photoEmpty;
std::vector<Participant> _participants;
bool _isChannel = false;
bool _requestApprove = false;
int _userWidth = 0;
};

View File

@@ -0,0 +1,170 @@
/*
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 "api/api_chat_links.h"
#include "api/api_text_entities.h"
#include "apiwrap.h"
#include "data/data_session.h"
#include "main/main_session.h"
namespace Api {
namespace {
[[nodiscard]] ChatLink FromMTP(
not_null<Main::Session*> session,
const MTPBusinessChatLink &link) {
const auto &data = link.data();
return {
.link = qs(data.vlink()),
.title = qs(data.vtitle().value_or_empty()),
.message = {
qs(data.vmessage()),
EntitiesFromMTP(
session,
data.ventities().value_or_empty())
},
.clicks = data.vviews().v,
};
}
[[nodiscard]] MTPInputBusinessChatLink ToMTP(
not_null<Main::Session*> session,
const QString &title,
const TextWithEntities &message) {
auto entities = EntitiesToMTP(
session,
message.entities,
ConvertOption::SkipLocal);
using Flag = MTPDinputBusinessChatLink::Flag;
const auto flags = (title.isEmpty() ? Flag() : Flag::f_title)
| (entities.v.isEmpty() ? Flag() : Flag::f_entities);
return MTP_inputBusinessChatLink(
MTP_flags(flags),
MTP_string(message.text),
std::move(entities),
MTP_string(title));
}
} // namespace
ChatLinks::ChatLinks(not_null<ApiWrap*> api) : _api(api) {
}
void ChatLinks::create(
const QString &title,
const TextWithEntities &message,
Fn<void(Link)> done) {
const auto session = &_api->session();
_api->request(MTPaccount_CreateBusinessChatLink(
ToMTP(session, title, message)
)).done([=](const MTPBusinessChatLink &result) {
const auto link = FromMTP(session, result);
_list.push_back(link);
_updates.fire({ .was = QString(), .now = link });
if (done) done(link);
}).fail([=](const MTP::Error &error) {
const auto type = error.type();
if (done) done(Link());
}).send();
}
void ChatLinks::edit(
const QString &link,
const QString &title,
const TextWithEntities &message,
Fn<void(Link)> done) {
const auto session = &_api->session();
_api->request(MTPaccount_EditBusinessChatLink(
MTP_string(link),
ToMTP(session, title, message)
)).done([=](const MTPBusinessChatLink &result) {
const auto parsed = FromMTP(session, result);
if (parsed.link != link) {
LOG(("API Error: EditBusinessChatLink changed the link."));
if (done) done(Link());
return;
}
const auto i = ranges::find(_list, link, &Link::link);
if (i != end(_list)) {
*i = parsed;
_updates.fire({ .was = link, .now = parsed });
if (done) done(parsed);
} else {
LOG(("API Error: EditBusinessChatLink link not found."));
if (done) done(Link());
}
}).fail([=](const MTP::Error &error) {
const auto type = error.type();
if (done) done(Link());
}).send();
}
void ChatLinks::destroy(
const QString &link,
Fn<void()> done) {
_api->request(MTPaccount_DeleteBusinessChatLink(
MTP_string(link)
)).done([=] {
const auto i = ranges::find(_list, link, &Link::link);
if (i != end(_list)) {
_list.erase(i);
_updates.fire({ .was = link });
if (done) done();
} else {
LOG(("API Error: DeleteBusinessChatLink link not found."));
if (done) done();
}
}).fail([=](const MTP::Error &error) {
const auto type = error.type();
if (done) done();
}).send();
}
void ChatLinks::preload() {
if (_loaded || _requestId) {
return;
}
_requestId = _api->request(MTPaccount_GetBusinessChatLinks(
)).done([=](const MTPaccount_BusinessChatLinks &result) {
const auto &data = result.data();
const auto session = &_api->session();
const auto owner = &session->data();
owner->processUsers(data.vusers());
owner->processChats(data.vchats());
auto links = std::vector<Link>();
links.reserve(data.vlinks().v.size());
for (const auto &link : data.vlinks().v) {
links.push_back(FromMTP(session, link));
}
_list = std::move(links);
_loaded = true;
_loadedUpdates.fire({});
}).fail([=] {
_requestId = 0;
_loaded = true;
_loadedUpdates.fire({});
}).send();
}
const std::vector<ChatLink> &ChatLinks::list() const {
return _list;
}
bool ChatLinks::loaded() const {
return _loaded;
}
rpl::producer<> ChatLinks::loadedUpdates() const {
return _loadedUpdates.events();
}
rpl::producer<ChatLinks::Update> ChatLinks::updates() const {
return _updates.events();
}
} // namespace Api

View File

@@ -0,0 +1,64 @@
/*
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
*/
#pragma once
class ApiWrap;
namespace Api {
struct ChatLink {
QString link;
QString title;
TextWithEntities message;
int clicks = 0;
};
struct ChatLinkUpdate {
QString was;
std::optional<ChatLink> now;
};
class ChatLinks final {
public:
explicit ChatLinks(not_null<ApiWrap*> api);
using Link = ChatLink;
using Update = ChatLinkUpdate;
void create(
const QString &title,
const TextWithEntities &message,
Fn<void(Link)> done = nullptr);
void edit(
const QString &link,
const QString &title,
const TextWithEntities &message,
Fn<void(Link)> done = nullptr);
void destroy(
const QString &link,
Fn<void()> done = nullptr);
void preload();
[[nodiscard]] const std::vector<ChatLink> &list() const;
[[nodiscard]] bool loaded() const;
[[nodiscard]] rpl::producer<> loadedUpdates() const;
[[nodiscard]] rpl::producer<Update> updates() const;
private:
const not_null<ApiWrap*> _api;
std::vector<Link> _list;
rpl::event_stream<> _loadedUpdates;
mtpRequestId _requestId = 0;
bool _loaded = false;
rpl::event_stream<Update> _updates;
};
} // namespace Api

View File

@@ -0,0 +1,891 @@
/*
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 "api/api_chat_participants.h"
#include "apiwrap.h"
#include "boxes/add_contact_box.h" // ShowAddParticipantsError
#include "boxes/peers/add_participants_box.h" // ChatInviteForbidden
#include "data/data_changes.h"
#include "data/data_channel.h"
#include "data/data_channel_admins.h"
#include "data/data_chat.h"
#include "data/data_histories.h"
#include "data/data_session.h"
#include "data/data_user.h"
#include "history/history.h"
#include "main/main_session.h"
#include "mtproto/mtproto_config.h"
namespace Api {
namespace {
using Members = ChatParticipants::Members;
constexpr auto kSmallDelayMs = crl::time(5);
// 1 second wait before reload members in channel after adding.
constexpr auto kReloadChannelMembersTimeout = 1000;
// Max users in one super group invite request.
constexpr auto kMaxUsersPerInvite = 100;
// How many messages from chat history server should forward to user,
// that was added to this chat.
constexpr auto kForwardMessagesOnAdd = 100;
std::vector<ChatParticipant> ParseList(
const ChatParticipants::TLMembers &data,
not_null<PeerData*> peer) {
return ranges::views::all(
data.vparticipants().v
) | ranges::views::transform([&](const MTPChannelParticipant &p) {
return ChatParticipant(p, peer);
}) | ranges::to_vector;
}
void ApplyMegagroupAdmins(not_null<ChannelData*> channel, Members list) {
Expects(channel->isMegagroup());
const auto i = ranges::find_if(list, &Api::ChatParticipant::isCreator);
if (i != list.end()) {
i->tryApplyCreatorTo(channel);
} else {
channel->mgInfo->creator = nullptr;
channel->mgInfo->creatorRank = QString();
}
auto adding = base::flat_map<UserId, QString>();
for (const auto &p : list) {
if (p.isUser()) {
adding.emplace(p.userId(), p.rank());
}
}
if (channel->mgInfo->creator) {
adding.emplace(
peerToUser(channel->mgInfo->creator->id),
channel->mgInfo->creatorRank);
}
auto removing = channel->mgInfo->admins;
if (removing.empty() && adding.empty()) {
// Add some admin-placeholder so we don't DDOS
// server with admins list requests.
LOG(("API Error: Got empty admins list from server."));
adding.emplace(0, QString());
}
Data::ChannelAdminChanges changes(channel);
for (const auto &[addingId, rank] : adding) {
if (!removing.remove(addingId)) {
changes.add(addingId, rank);
}
}
for (const auto &[removingId, rank] : removing) {
changes.remove(removingId);
}
}
void RefreshChannelAdmins(
not_null<ChannelData*> channel,
Members participants) {
Data::ChannelAdminChanges changes(channel);
for (const auto &p : participants) {
if (p.isUser()) {
if (p.isCreatorOrAdmin()) {
p.tryApplyCreatorTo(channel);
changes.add(p.userId(), p.rank());
} else {
changes.remove(p.userId());
}
}
}
}
void ApplyLastList(
not_null<ChannelData*> channel,
int availableCount,
Members list) {
channel->mgInfo->lastAdmins.clear();
channel->mgInfo->lastRestricted.clear();
channel->mgInfo->lastParticipants.clear();
channel->mgInfo->lastParticipantsStatus
= MegagroupInfo::LastParticipantsUpToDate
| MegagroupInfo::LastParticipantsOnceReceived;
auto botStatus = channel->mgInfo->botStatus;
for (const auto &p : list) {
const auto participant = channel->owner().peer(p.id());
const auto user = participant->asUser();
const auto adminRights = p.rights();
const auto restrictedRights = p.restrictions();
if (p.isCreator()) {
Assert(user != nullptr);
p.tryApplyCreatorTo(channel);
if (!channel->mgInfo->admins.empty()) {
Data::ChannelAdminChanges(channel).add(p.userId(), p.rank());
}
}
if (user
&& !base::contains(channel->mgInfo->lastParticipants, user)) {
channel->mgInfo->lastParticipants.push_back(user);
if (adminRights.flags) {
channel->mgInfo->lastAdmins.emplace(
user,
MegagroupInfo::Admin{ adminRights, p.canBeEdited() });
} else if (restrictedRights.flags) {
channel->mgInfo->lastRestricted.emplace(
user,
MegagroupInfo::Restricted{ restrictedRights });
}
if (user->isBot()) {
channel->mgInfo->bots.insert(user);
if ((channel->mgInfo->botStatus != 0)
&& (channel->mgInfo->botStatus < 2)) {
channel->mgInfo->botStatus = 2;
}
}
}
}
//
// getParticipants(Recent) sometimes can't return all members,
// only some last subset, size of this subset is availableCount.
//
// So both list size and availableCount have nothing to do with
// the full supergroup members count.
//
//if (list.isEmpty()) {
// channel->setMembersCount(channel->mgInfo->lastParticipants.size());
//} else {
// channel->setMembersCount(availableCount);
//}
channel->session().changes().peerUpdated(
channel,
(Data::PeerUpdate::Flag::Members | Data::PeerUpdate::Flag::Admins));
channel->mgInfo->botStatus = botStatus;
channel->session().changes().peerUpdated(
channel,
Data::PeerUpdate::Flag::FullInfo);
}
void ApplyBotsList(
not_null<ChannelData*> channel,
int availableCount,
Members list) {
const auto history = channel->owner().historyLoaded(channel);
channel->mgInfo->bots.clear();
channel->mgInfo->botStatus = -1;
auto needBotsInfos = false;
auto botStatus = channel->mgInfo->botStatus;
auto keyboardBotFound = !history || !history->lastKeyboardFrom;
for (const auto &p : list) {
const auto participant = channel->owner().peer(p.id());
const auto user = participant->asUser();
if (user && user->isBot()) {
channel->mgInfo->bots.insert(user);
botStatus = 2;// (botStatus > 0/* || !i.key()->botInfo->readsAllHistory*/) ? 2 : 1;
if (!user->botInfo->inited) {
needBotsInfos = true;
}
}
if (!keyboardBotFound
&& participant->id == history->lastKeyboardFrom) {
keyboardBotFound = true;
}
}
if (needBotsInfos) {
channel->session().api().requestFullPeer(channel);
}
if (!keyboardBotFound) {
history->clearLastKeyboard();
}
channel->mgInfo->botStatus = botStatus;
channel->session().changes().peerUpdated(
channel,
Data::PeerUpdate::Flag::FullInfo);
}
[[nodiscard]] ChatParticipants::Peers ParseSimilarChannels(
not_null<Main::Session*> session,
const MTPmessages_Chats &chats) {
auto result = ChatParticipants::Peers();
chats.match([&](const auto &data) {
const auto &list = data.vchats().v;
result.list.reserve(list.size());
for (const auto &chat : list) {
const auto peer = session->data().processChat(chat);
if (const auto channel = peer->asChannel()) {
result.list.push_back(channel);
}
}
if constexpr (MTPDmessages_chatsSlice::Is<decltype(data)>()) {
if (session->premiumPossible()) {
result.more = data.vcount().v - data.vchats().v.size();
}
}
});
return result;
}
[[nodiscard]] ChatParticipants::Peers ParseSimilarChannels(
not_null<ChannelData*> channel,
const MTPmessages_Chats &chats) {
return ParseSimilarChannels(&channel->session(), chats);
}
[[nodiscard]] ChatParticipants::Peers ParseSimilarBots(
not_null<Main::Session*> session,
const MTPusers_Users &users) {
auto result = ChatParticipants::Peers();
users.match([&](const auto &data) {
const auto &list = data.vusers().v;
result.list.reserve(list.size());
for (const auto &user : list) {
result.list.push_back(session->data().processUser(user));
}
if constexpr (MTPDusers_usersSlice::Is<decltype(data)>()) {
if (session->premiumPossible()) {
result.more = data.vcount().v - data.vusers().v.size();
}
}
});
return result;
}
} // namespace
ChatParticipant::ChatParticipant(
const MTPChannelParticipant &p,
not_null<PeerData*> peer) {
_peer = p.match([](const MTPDchannelParticipantBanned &data) {
return peerFromMTP(data.vpeer());
}, [](const MTPDchannelParticipantLeft &data) {
return peerFromMTP(data.vpeer());
}, [](const auto &data) {
return peerFromUser(data.vuser_id());
});
p.match([&](const MTPDchannelParticipantCreator &data) {
_canBeEdited = (peer->session().userPeerId() == _peer);
_type = Type::Creator;
_rights = ChatAdminRightsInfo(data.vadmin_rights());
_rank = qs(data.vrank().value_or_empty());
}, [&](const MTPDchannelParticipantAdmin &data) {
_canBeEdited = data.is_can_edit();
_type = Type::Admin;
_rank = qs(data.vrank().value_or_empty());
_rights = ChatAdminRightsInfo(data.vadmin_rights());
_by = peerToUser(peerFromUser(data.vpromoted_by()));
_date = data.vdate().v;
}, [&](const MTPDchannelParticipantSelf &data) {
_type = Type::Member;
_date = data.vdate().v;
_by = peerToUser(peerFromUser(data.vinviter_id()));
if (data.vsubscription_until_date()) {
_subscriptionDate = data.vsubscription_until_date()->v;
}
}, [&](const MTPDchannelParticipant &data) {
_type = Type::Member;
_date = data.vdate().v;
if (data.vsubscription_until_date()) {
_subscriptionDate = data.vsubscription_until_date()->v;
}
}, [&](const MTPDchannelParticipantBanned &data) {
_restrictions = ChatRestrictionsInfo(data.vbanned_rights());
_by = peerToUser(peerFromUser(data.vkicked_by()));
_date = data.vdate().v;
_type = (_restrictions.flags & ChatRestriction::ViewMessages)
? Type::Banned
: Type::Restricted;
}, [&](const MTPDchannelParticipantLeft &data) {
_type = Type::Left;
});
}
ChatParticipant::ChatParticipant(
Type type,
PeerId peerId,
UserId by,
ChatRestrictionsInfo restrictions,
ChatAdminRightsInfo rights,
bool canBeEdited,
QString rank)
: _type(type)
, _peer(peerId)
, _by(by)
, _canBeEdited(canBeEdited)
, _rank(rank)
, _restrictions(std::move(restrictions))
, _rights(std::move(rights)) {
}
void ChatParticipant::tryApplyCreatorTo(
not_null<ChannelData*> channel) const {
if (isCreator() && isUser()) {
if (const auto info = channel->mgInfo.get()) {
info->creator = channel->owner().userLoaded(userId());
info->creatorRank = rank();
}
}
}
bool ChatParticipant::isUser() const {
return peerIsUser(_peer);
}
bool ChatParticipant::isCreator() const {
return _type == Type::Creator;
}
bool ChatParticipant::isCreatorOrAdmin() const {
return _type == Type::Creator || _type == Type::Admin;
}
bool ChatParticipant::isKicked() const {
return _type == Type::Banned;
}
bool ChatParticipant::canBeEdited() const {
return _canBeEdited;
}
UserId ChatParticipant::by() const {
return _by;
}
PeerId ChatParticipant::id() const {
return _peer;
}
UserId ChatParticipant::userId() const {
return peerToUser(_peer);
}
ChatRestrictionsInfo ChatParticipant::restrictions() const {
return _restrictions;
}
ChatAdminRightsInfo ChatParticipant::rights() const {
return _rights;
}
TimeId ChatParticipant::subscriptionDate() const {
return _subscriptionDate;
}
TimeId ChatParticipant::promotedSince() const {
return (_type == Type::Admin) ? _date : TimeId(0);
}
TimeId ChatParticipant::restrictedSince() const {
return (_type == Type::Restricted || _type == Type::Banned)
? _date
: TimeId(0);
}
TimeId ChatParticipant::memberSince() const {
return (_type == Type::Member) ? _date : TimeId(0);
}
ChatParticipant::Type ChatParticipant::type() const {
return _type;
}
QString ChatParticipant::rank() const {
return _rank;
}
ChatParticipants::ChatParticipants(not_null<ApiWrap*> api)
: _session(&api->session())
, _api(&api->instance()) {
}
void ChatParticipants::requestForAdd(
not_null<ChannelData*> channel,
Fn<void(const TLMembers&)> callback) {
Expects(callback != nullptr);
_forAdd.callback = std::move(callback);
if (_forAdd.channel == channel) {
return;
}
_api.request(base::take(_forAdd.requestId)).cancel();
const auto offset = 0;
const auto participantsHash = uint64(0);
_forAdd.channel = channel;
_forAdd.requestId = _api.request(MTPchannels_GetParticipants(
channel->inputChannel(),
MTP_channelParticipantsRecent(),
MTP_int(offset),
MTP_int(channel->session().serverConfig().chatSizeMax),
MTP_long(participantsHash)
)).done([=](const MTPchannels_ChannelParticipants &result) {
result.match([&](const MTPDchannels_channelParticipants &data) {
base::take(_forAdd).callback(data);
}, [&](const MTPDchannels_channelParticipantsNotModified &) {
base::take(_forAdd);
LOG(("API Error: "
"channels.channelParticipantsNotModified received!"));
});
}).fail([=] {
base::take(_forAdd);
}).send();
}
void ChatParticipants::requestLast(not_null<ChannelData*> channel) {
if (!channel->isMegagroup()
|| !channel->canViewMembers()
|| _participantsRequests.contains(channel)) {
return;
}
const auto offset = 0;
const auto participantsHash = uint64(0);
const auto requestId = _api.request(MTPchannels_GetParticipants(
channel->inputChannel(),
MTP_channelParticipantsRecent(),
MTP_int(offset),
MTP_int(channel->session().serverConfig().chatSizeMax),
MTP_long(participantsHash)
)).done([=](const MTPchannels_ChannelParticipants &result) {
_participantsRequests.remove(channel);
result.match([&](const MTPDchannels_channelParticipants &data) {
const auto &[availableCount, list] = Parse(channel, data);
ApplyLastList(channel, availableCount, list);
}, [](const MTPDchannels_channelParticipantsNotModified &) {
LOG(("API Error: "
"channels.channelParticipantsNotModified received!"));
});
}).fail([this, channel] {
_participantsRequests.remove(channel);
}).send();
_participantsRequests[channel] = requestId;
}
void ChatParticipants::requestBots(not_null<ChannelData*> channel) {
if (!channel->isMegagroup() || _botsRequests.contains(channel)) {
return;
}
const auto offset = 0;
const auto participantsHash = uint64(0);
const auto requestId = _api.request(MTPchannels_GetParticipants(
channel->inputChannel(),
MTP_channelParticipantsBots(),
MTP_int(offset),
MTP_int(channel->session().serverConfig().chatSizeMax),
MTP_long(participantsHash)
)).done([=](const MTPchannels_ChannelParticipants &result) {
_botsRequests.remove(channel);
result.match([&](const MTPDchannels_channelParticipants &data) {
const auto &[availableCount, list] = Parse(channel, data);
ApplyBotsList(channel, availableCount, list);
}, [](const MTPDchannels_channelParticipantsNotModified &) {
LOG(("API Error: "
"channels.channelParticipantsNotModified received!"));
});
}).fail([=](const MTP::Error &error) {
_botsRequests.remove(channel);
if (error.type() == u"CHANNEL_MONOFORUM_UNSUPPORTED"_q) {
channel->mgInfo->bots.clear();
channel->mgInfo->botStatus = -1;
channel->session().changes().peerUpdated(
channel,
Data::PeerUpdate::Flag::FullInfo);
}
}).send();
_botsRequests[channel] = requestId;
}
void ChatParticipants::requestAdmins(not_null<ChannelData*> channel) {
if (!channel->isMegagroup() || _adminsRequests.contains(channel)) {
return;
}
const auto offset = 0;
const auto participantsHash = uint64(0);
const auto requestId = _api.request(MTPchannels_GetParticipants(
channel->inputChannel(),
MTP_channelParticipantsAdmins(),
MTP_int(offset),
MTP_int(channel->session().serverConfig().chatSizeMax),
MTP_long(participantsHash)
)).done([=](const MTPchannels_ChannelParticipants &result) {
channel->mgInfo->adminsLoaded = true;
_adminsRequests.remove(channel);
result.match([&](const MTPDchannels_channelParticipants &data) {
channel->owner().processUsers(data.vusers());
ApplyMegagroupAdmins(channel, ParseList(data, channel));
}, [](const MTPDchannels_channelParticipantsNotModified &) {
LOG(("API Error: "
"channels.channelParticipantsNotModified received!"));
});
}).fail([=] {
channel->mgInfo->adminsLoaded = true;
_adminsRequests.remove(channel);
}).send();
_adminsRequests[channel] = requestId;
}
void ChatParticipants::requestCountDelayed(
not_null<ChannelData*> channel) {
_participantsCountRequestTimer.call(
kReloadChannelMembersTimeout,
[=] { channel->updateFullForced(); });
}
void ChatParticipants::add(
std::shared_ptr<Ui::Show> show,
not_null<PeerData*> peer,
const std::vector<not_null<UserData*>> &users,
bool passGroupHistory,
Fn<void(bool)> done) {
if (const auto chat = peer->asChat()) {
for (const auto &user : users) {
_api.request(MTPmessages_AddChatUser(
chat->inputChat(),
user->inputUser(),
MTP_int(passGroupHistory ? kForwardMessagesOnAdd : 0)
)).done([=](const MTPmessages_InvitedUsers &result) {
const auto &data = result.data();
chat->session().api().applyUpdates(data.vupdates());
if (done) done(true);
ChatInviteForbidden(
show,
chat,
CollectForbiddenUsers(&chat->session(), result));
}).fail([=](const MTP::Error &error) {
const auto type = error.type();
ShowAddParticipantsError(show, type, peer, user);
if (done) done(false);
}).afterDelay(kSmallDelayMs).send();
}
} else if (const auto channel = peer->asChannel()) {
const auto hasBot = ranges::any_of(users, &UserData::isBot);
if (!peer->isMegagroup() && hasBot) {
ShowAddParticipantsError(
show,
u"USER_BOT"_q,
peer,
{ .users = users });
return;
}
auto list = QVector<MTPInputUser>();
list.reserve(std::min(int(users.size()), int(kMaxUsersPerInvite)));
const auto send = [&] {
const auto callback = base::take(done);
_api.request(MTPchannels_InviteToChannel(
channel->inputChannel(),
MTP_vector<MTPInputUser>(list)
)).done([=](const MTPmessages_InvitedUsers &result) {
const auto &data = result.data();
channel->session().api().applyUpdates(data.vupdates());
requestCountDelayed(channel);
if (callback) callback(true);
ChatInviteForbidden(
show,
channel,
CollectForbiddenUsers(&channel->session(), result));
}).fail([=](const MTP::Error &error) {
ShowAddParticipantsError(show, error.type(), peer, {
.users = users,
});
if (callback) callback(false);
}).afterDelay(kSmallDelayMs).send();
};
for (const auto &user : users) {
list.push_back(user->inputUser());
if (list.size() == kMaxUsersPerInvite) {
send();
list.clear();
}
}
if (!list.empty()) {
send();
}
} else {
Unexpected("User in ChatParticipants::add.");
}
}
ChatParticipants::Parsed ChatParticipants::Parse(
not_null<ChannelData*> channel,
const TLMembers &data) {
channel->owner().processUsers(data.vusers());
channel->owner().processChats(data.vchats());
auto list = ParseList(data, channel);
if (channel->mgInfo) {
RefreshChannelAdmins(channel, list);
}
return { data.vcount().v, std::move(list) };
}
ChatParticipants::Parsed ChatParticipants::ParseRecent(
not_null<ChannelData*> channel,
const TLMembers &data) {
const auto result = Parse(channel, data);
const auto applyLast = channel->isMegagroup()
&& channel->canViewMembers()
&& (channel->mgInfo->lastParticipants.size() <= result.list.size());
if (applyLast) {
ApplyLastList(channel, result.availableCount, result.list);
}
return result;
}
void ChatParticipants::Restrict(
not_null<ChannelData*> channel,
not_null<PeerData*> participant,
ChatRestrictionsInfo oldRights,
ChatRestrictionsInfo newRights,
Fn<void()> onDone,
Fn<void()> onFail) {
channel->session().api().request(MTPchannels_EditBanned(
channel->inputChannel(),
participant->input(),
RestrictionsToMTP(newRights)
)).done([=](const MTPUpdates &result) {
channel->session().api().applyUpdates(result);
channel->applyEditBanned(participant, oldRights, newRights);
if (onDone) {
onDone();
}
}).fail([=] {
if (onFail) {
onFail();
}
}).send();
}
void ChatParticipants::requestSelf(not_null<ChannelData*> channel) {
if (_selfParticipantRequests.contains(channel)) {
return;
}
const auto finalize = [=](
UserId inviter = -1,
TimeId inviteDate = 0,
bool inviteViaRequest = false) {
channel->inviter = inviter;
channel->inviteDate = inviteDate;
channel->inviteViaRequest = inviteViaRequest;
if (const auto history = channel->owner().historyLoaded(channel)) {
if (history->lastMessageKnown()) {
history->checkLocalMessages();
history->owner().sendHistoryChangeNotifications();
} else {
history->owner().histories().requestDialogEntry(history);
}
}
};
_selfParticipantRequests.emplace(channel);
_api.request(MTPchannels_GetParticipant(
channel->inputChannel(),
MTP_inputPeerSelf()
)).done([=](const MTPchannels_ChannelParticipant &result) {
_selfParticipantRequests.erase(channel);
result.match([&](const MTPDchannels_channelParticipant &data) {
channel->owner().processUsers(data.vusers());
const auto &participant = data.vparticipant();
participant.match([&](const MTPDchannelParticipantSelf &data) {
finalize(
data.vinviter_id().v,
data.vdate().v,
data.is_via_request());
}, [&](const MTPDchannelParticipantCreator &) {
if (channel->mgInfo) {
channel->mgInfo->creator = channel->session().user();
}
finalize(channel->session().userId(), channel->date);
}, [&](const MTPDchannelParticipantAdmin &data) {
const auto inviter = data.is_self()
? data.vinviter_id().value_or(-1)
: -1;
finalize(inviter, data.vdate().v);
}, [&](const MTPDchannelParticipantBanned &data) {
LOG(("API Error: Got self banned participant."));
finalize();
}, [&](const MTPDchannelParticipant &data) {
LOG(("API Error: Got self regular participant."));
finalize();
}, [&](const MTPDchannelParticipantLeft &data) {
LOG(("API Error: Got self left participant."));
finalize();
});
});
}).fail([=](const MTP::Error &error) {
_selfParticipantRequests.erase(channel);
if (error.type() == u"CHANNEL_PRIVATE"_q) {
channel->privateErrorReceived();
}
finalize();
}).afterDelay(kSmallDelayMs).send();
}
void ChatParticipants::kick(
not_null<ChatData*> chat,
not_null<PeerData*> participant) {
Expects(participant->isUser());
_api.request(MTPmessages_DeleteChatUser(
MTP_flags(0),
chat->inputChat(),
participant->asUser()->inputUser()
)).done([=](const MTPUpdates &result) {
chat->session().api().applyUpdates(result);
}).send();
}
void ChatParticipants::kick(
not_null<ChannelData*> channel,
not_null<PeerData*> participant,
ChatRestrictionsInfo currentRights) {
const auto kick = KickRequest(channel, participant);
if (_kickRequests.contains(kick)) return;
const auto rights = ChannelData::KickedRestrictedRights(participant);
const auto requestId = _api.request(MTPchannels_EditBanned(
channel->inputChannel(),
participant->input(),
RestrictionsToMTP(rights)
)).done([=](const MTPUpdates &result) {
channel->session().api().applyUpdates(result);
_kickRequests.remove(KickRequest(channel, participant));
channel->applyEditBanned(participant, currentRights, rights);
}).fail([this, kick] {
_kickRequests.remove(kick);
}).send();
_kickRequests.emplace(kick, requestId);
}
void ChatParticipants::unblock(
not_null<ChannelData*> channel,
not_null<PeerData*> participant) {
const auto kick = KickRequest(channel, participant);
if (_kickRequests.contains(kick)) {
return;
}
const auto requestId = _api.request(MTPchannels_EditBanned(
channel->inputChannel(),
participant->input(),
MTP_chatBannedRights(MTP_flags(0), MTP_int(0))
)).done([=](const MTPUpdates &result) {
channel->session().api().applyUpdates(result);
_kickRequests.remove(KickRequest(channel, participant));
if (channel->kickedCount() > 0) {
channel->setKickedCount(channel->kickedCount() - 1);
} else {
channel->updateFullForced();
}
}).fail([=] {
_kickRequests.remove(kick);
}).send();
_kickRequests.emplace(kick, requestId);
}
void ChatParticipants::loadSimilarPeers(not_null<PeerData*> peer) {
if (const auto i = _similar.find(peer); i != end(_similar)) {
if (i->second.requestId
|| !i->second.peers.more
|| !peer->session().premium()) {
return;
}
}
if (const auto channel = peer->asBroadcast()) {
using Flag = MTPchannels_GetChannelRecommendations::Flag;
_similar[peer].requestId = _api.request(
MTPchannels_GetChannelRecommendations(
MTP_flags(Flag::f_channel),
channel->inputChannel())
).done([=](const MTPmessages_Chats &result) {
auto &similar = _similar[channel];
similar.requestId = 0;
auto parsed = ParseSimilarChannels(channel, result);
if (similar.peers == parsed) {
return;
}
similar.peers = std::move(parsed);
if (const auto history = channel->owner().historyLoaded(channel)) {
if (const auto item = history->joinedMessageInstance()) {
history->owner().requestItemResize(item);
}
}
_similarLoaded.fire_copy(channel);
}).send();
} else if (const auto bot = peer->asBot()) {
_similar[peer].requestId = _api.request(
MTPbots_GetBotRecommendations(bot->inputUser())
).done([=](const MTPusers_Users &result) {
auto &similar = _similar[peer];
similar.requestId = 0;
auto parsed = ParseSimilarBots(&peer->session(), result);
if (similar.peers == parsed) {
return;
}
similar.peers = std::move(parsed);
_similarLoaded.fire_copy(peer);
}).send();
}
}
auto ChatParticipants::similar(not_null<PeerData*> peer)
-> const Peers & {
const auto i = (peer->isBroadcast() || peer->isBot())
? _similar.find(peer)
: end(_similar);
if (i != end(_similar)) {
return i->second.peers;
}
static const auto empty = Peers();
return empty;
}
auto ChatParticipants::similarLoaded() const
-> rpl::producer<not_null<PeerData*>> {
return _similarLoaded.events();
}
void ChatParticipants::loadRecommendations() {
if (_recommendationsLoaded.current() || _recommendations.requestId) {
return;
}
_recommendations.requestId = _api.request(
MTPchannels_GetChannelRecommendations(
MTP_flags(0),
MTP_inputChannelEmpty())
).done([=](const MTPmessages_Chats &result) {
_recommendations.requestId = 0;
auto parsed = ParseSimilarChannels(_session, result);
_recommendations.peers = std::move(parsed);
_recommendations.peers.more = 0;
_recommendationsLoaded = true;
}).send();
}
const ChatParticipants::Peers &ChatParticipants::recommendations() const {
return _recommendations.peers;
}
rpl::producer<> ChatParticipants::recommendationsLoaded() const {
return _recommendationsLoaded.changes() | rpl::to_empty;
}
} // namespace Api

View File

@@ -0,0 +1,197 @@
/*
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
*/
#pragma once
#include "data/data_chat_participant_status.h"
#include "mtproto/sender.h"
#include "base/timer.h"
class ApiWrap;
class ChannelData;
namespace Main {
class Session;
} // namespace Main
namespace Ui {
class Show;
} // namespace Ui
namespace Api {
class ChatParticipant final {
public:
enum class Type {
Creator,
Admin,
Member,
Restricted,
Left,
Banned,
};
explicit ChatParticipant(
const MTPChannelParticipant &p,
not_null<PeerData*> peer);
ChatParticipant(
Type type,
PeerId peerId,
UserId by,
ChatRestrictionsInfo restrictions,
ChatAdminRightsInfo rights,
bool canBeEdited = false,
QString rank = QString());
bool isUser() const;
bool isCreator() const;
bool isCreatorOrAdmin() const;
bool isKicked() const;
bool canBeEdited() const;
UserId by() const;
PeerId id() const;
UserId userId() const;
ChatRestrictionsInfo restrictions() const;
ChatAdminRightsInfo rights() const;
TimeId subscriptionDate() const;
TimeId promotedSince() const;
TimeId restrictedSince() const;
TimeId memberSince() const;
Type type() const;
QString rank() const;
void tryApplyCreatorTo(not_null<ChannelData*> channel) const;
private:
Type _type = Type::Member;
PeerId _peer;
UserId _by; // Banned/Restricted/Promoted.
bool _canBeEdited = false;
QString _rank;
TimeId _subscriptionDate = 0;
TimeId _date = 0;
ChatRestrictionsInfo _restrictions;
ChatAdminRightsInfo _rights;
};
class ChatParticipants final {
public:
struct Parsed {
const int availableCount;
const std::vector<ChatParticipant> list;
};
using TLMembers = MTPDchannels_channelParticipants;
using Members = const std::vector<ChatParticipant> &;
explicit ChatParticipants(not_null<ApiWrap*> api);
void requestLast(not_null<ChannelData*> channel);
void requestBots(not_null<ChannelData*> channel);
void requestAdmins(not_null<ChannelData*> channel);
void requestCountDelayed(not_null<ChannelData*> channel);
static Parsed Parse(
not_null<ChannelData*> channel,
const TLMembers &data);
static Parsed ParseRecent(
not_null<ChannelData*> channel,
const TLMembers &data);
static void Restrict(
not_null<ChannelData*> channel,
not_null<PeerData*> participant,
ChatRestrictionsInfo oldRights,
ChatRestrictionsInfo newRights,
Fn<void()> onDone,
Fn<void()> onFail);
void add(
std::shared_ptr<Ui::Show> show,
not_null<PeerData*> peer,
const std::vector<not_null<UserData*>> &users,
bool passGroupHistory = true,
Fn<void(bool)> done = nullptr);
void requestSelf(not_null<ChannelData*> channel);
void requestForAdd(
not_null<ChannelData*> channel,
Fn<void(const TLMembers&)> callback);
void kick(
not_null<ChatData*> chat,
not_null<PeerData*> participant);
void kick(
not_null<ChannelData*> channel,
not_null<PeerData*> participant,
ChatRestrictionsInfo currentRights);
void unblock(
not_null<ChannelData*> channel,
not_null<PeerData*> participant);
void loadSimilarPeers(not_null<PeerData*> peer);
struct Peers {
std::vector<not_null<PeerData*>> list;
int more = 0;
friend inline bool operator==(
const Peers &,
const Peers &) = default;
};
[[nodiscard]] const Peers &similar(not_null<PeerData*> peer);
[[nodiscard]] auto similarLoaded() const
-> rpl::producer<not_null<PeerData*>>;
void loadRecommendations();
[[nodiscard]] const Peers &recommendations() const;
[[nodiscard]] rpl::producer<> recommendationsLoaded() const;
private:
struct SimilarPeers {
Peers peers;
mtpRequestId requestId = 0;
};
const not_null<Main::Session*> _session;
MTP::Sender _api;
using PeerRequests = base::flat_map<PeerData*, mtpRequestId>;
PeerRequests _participantsRequests;
PeerRequests _botsRequests;
PeerRequests _adminsRequests;
base::DelayedCallTimer _participantsCountRequestTimer;
struct {
ChannelData *channel = nullptr;
mtpRequestId requestId = 0;
Fn<void(const TLMembers&)> callback;
} _forAdd;
base::flat_set<not_null<ChannelData*>> _selfParticipantRequests;
using KickRequest = std::pair<
not_null<ChannelData*>,
not_null<PeerData*>>;
base::flat_map<KickRequest, mtpRequestId> _kickRequests;
base::flat_map<not_null<PeerData*>, SimilarPeers> _similar;
rpl::event_stream<not_null<PeerData*>> _similarLoaded;
SimilarPeers _recommendations;
rpl::variable<bool> _recommendationsLoaded = false;
};
} // namespace Api

View File

@@ -0,0 +1,581 @@
/*
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 "api/api_cloud_password.h"
#include "apiwrap.h"
#include "base/random.h"
#include "core/core_cloud_password.h"
#include "passport/passport_encryption.h"
#include "base/unixtime.h"
#include "base/call_delayed.h"
namespace Api {
namespace {
[[nodiscard]] Core::CloudPasswordState ProcessMtpState(
const MTPaccount_password &state) {
return state.match([&](const MTPDaccount_password &data) {
base::RandomAddSeed(bytes::make_span(data.vsecure_random().v));
return Core::ParseCloudPasswordState(data);
});
}
} // namespace
CloudPassword::CloudPassword(not_null<ApiWrap*> api)
: _api(&api->instance()) {
}
void CloudPassword::apply(Core::CloudPasswordState state) {
if (_state) {
*_state = std::move(state);
} else {
_state = std::make_unique<Core::CloudPasswordState>(std::move(state));
}
_stateChanges.fire_copy(*_state);
}
void CloudPassword::reload() {
if (_requestId) {
return;
}
_requestId = _api.request(MTPaccount_GetPassword(
)).done([=](const MTPaccount_Password &result) {
_requestId = 0;
apply(ProcessMtpState(result));
}).fail([=] {
_requestId = 0;
}).send();
}
void CloudPassword::clearUnconfirmedPassword() {
_requestId = _api.request(MTPaccount_CancelPasswordEmail(
)).done([=] {
_requestId = 0;
reload();
}).fail([=] {
_requestId = 0;
reload();
}).send();
}
rpl::producer<Core::CloudPasswordState> CloudPassword::state() const {
return _state
? _stateChanges.events_starting_with_copy(*_state)
: (_stateChanges.events() | rpl::type_erased);
}
auto CloudPassword::stateCurrent() const
-> std::optional<Core::CloudPasswordState> {
return _state
? base::make_optional(*_state)
: std::nullopt;
}
auto CloudPassword::resetPassword()
-> rpl::producer<CloudPassword::ResetRetryDate, QString> {
return [=](auto consumer) {
_api.request(MTPaccount_ResetPassword(
)).done([=](const MTPaccount_ResetPasswordResult &result) {
result.match([&](const MTPDaccount_resetPasswordOk &data) {
reload();
}, [&](const MTPDaccount_resetPasswordRequestedWait &data) {
if (!_state) {
reload();
return;
}
const auto until = data.vuntil_date().v;
if (_state->pendingResetDate != until) {
_state->pendingResetDate = until;
_stateChanges.fire_copy(*_state);
}
}, [&](const MTPDaccount_resetPasswordFailedWait &data) {
consumer.put_next_copy(data.vretry_date().v);
});
consumer.put_done();
}).fail([=](const MTP::Error &error) {
consumer.put_error_copy(error.type());
}).send();
return rpl::lifetime();
};
}
auto CloudPassword::cancelResetPassword()
-> rpl::producer<rpl::no_value, QString> {
return [=](auto consumer) {
_api.request(MTPaccount_DeclinePasswordReset(
)).done([=] {
reload();
consumer.put_done();
}).fail([=](const MTP::Error &error) {
consumer.put_error_copy(error.type());
}).send();
return rpl::lifetime();
};
}
rpl::producer<CloudPassword::SetOk, QString> CloudPassword::set(
const QString &oldPassword,
const QString &newPassword,
const QString &hint,
bool hasRecoveryEmail,
const QString &recoveryEmail) {
const auto generatePasswordCheck = [=](
const Core::CloudPasswordState &latestState) {
if (oldPassword.isEmpty() || !latestState.hasPassword) {
return Core::CloudPasswordResult{
MTP_inputCheckPasswordEmpty()
};
}
const auto hash = Core::ComputeCloudPasswordHash(
latestState.mtp.request.algo,
bytes::make_span(oldPassword.toUtf8()));
return Core::ComputeCloudPasswordCheck(
latestState.mtp.request,
hash);
};
const auto finish = [=](auto consumer, int unconfirmedEmailLengthCode) {
_api.request(MTPaccount_GetPassword(
)).done([=](const MTPaccount_Password &result) {
apply(ProcessMtpState(result));
if (unconfirmedEmailLengthCode) {
consumer.put_next(SetOk{ unconfirmedEmailLengthCode });
} else {
consumer.put_done();
}
}).fail([=](const MTP::Error &error) {
consumer.put_error_copy(error.type());
}).handleFloodErrors().send();
};
const auto sendMTPaccountUpdatePasswordSettings = [=](
const Core::CloudPasswordState &latestState,
const QByteArray &secureSecret,
auto consumer) {
const auto newPasswordBytes = newPassword.toUtf8();
const auto newPasswordHash = Core::ComputeCloudPasswordDigest(
latestState.mtp.newPassword,
bytes::make_span(newPasswordBytes));
if (!newPassword.isEmpty() && newPasswordHash.modpow.empty()) {
consumer.put_error("INTERNAL_SERVER_ERROR");
return;
}
using Flag = MTPDaccount_passwordInputSettings::Flag;
const auto flags = Flag::f_new_algo
| Flag::f_new_password_hash
| Flag::f_hint
| (secureSecret.isEmpty() ? Flag(0) : Flag::f_new_secure_settings)
| ((!hasRecoveryEmail) ? Flag(0) : Flag::f_email);
auto newSecureSecret = bytes::vector();
auto newSecureSecretId = 0ULL;
if (!secureSecret.isEmpty()) {
newSecureSecretId = Passport::CountSecureSecretId(
bytes::make_span(secureSecret));
newSecureSecret = Passport::EncryptSecureSecret(
bytes::make_span(secureSecret),
Core::ComputeSecureSecretHash(
latestState.mtp.newSecureSecret,
bytes::make_span(newPasswordBytes)));
}
const auto settings = MTP_account_passwordInputSettings(
MTP_flags(flags),
Core::PrepareCloudPasswordAlgo(newPassword.isEmpty()
? v::null
: latestState.mtp.newPassword),
newPassword.isEmpty()
? MTP_bytes()
: MTP_bytes(newPasswordHash.modpow),
MTP_string(hint),
MTP_string(recoveryEmail),
MTP_secureSecretSettings(
Core::PrepareSecureSecretAlgo(
latestState.mtp.newSecureSecret),
MTP_bytes(newSecureSecret),
MTP_long(newSecureSecretId)));
_api.request(MTPaccount_UpdatePasswordSettings(
generatePasswordCheck(latestState).result,
settings
)).done([=] {
finish(consumer, 0);
}).fail([=](const MTP::Error &error) {
const auto &type = error.type();
const auto prefix = u"EMAIL_UNCONFIRMED_"_q;
if (type.startsWith(prefix)) {
const auto codeLength = base::StringViewMid(
type,
prefix.size()).toInt();
finish(consumer, codeLength);
} else {
consumer.put_error_copy(type);
}
}).handleFloodErrors().send();
};
return [=](auto consumer) {
_api.request(MTPaccount_GetPassword(
)).done([=](const MTPaccount_Password &result) {
const auto latestState = ProcessMtpState(result);
if (latestState.hasPassword
&& !oldPassword.isEmpty()
&& !newPassword.isEmpty()) {
_api.request(MTPaccount_GetPasswordSettings(
generatePasswordCheck(latestState).result
)).done([=](const MTPaccount_PasswordSettings &result) {
using Settings = MTPDaccount_passwordSettings;
const auto &data = result.match([&](
const Settings &data) -> const Settings & {
return data;
});
auto secureSecret = QByteArray();
if (const auto wrapped = data.vsecure_settings()) {
using Secure = MTPDsecureSecretSettings;
const auto &settings = wrapped->match([](
const Secure &data) -> const Secure & {
return data;
});
const auto passwordUtf = oldPassword.toUtf8();
const auto secret = Passport::DecryptSecureSecret(
bytes::make_span(settings.vsecure_secret().v),
Core::ComputeSecureSecretHash(
Core::ParseSecureSecretAlgo(
settings.vsecure_algo()),
bytes::make_span(passwordUtf)));
if (secret.empty()) {
LOG(("API Error: "
"Failed to decrypt secure secret."));
consumer.put_error("SUGGEST_SECRET_RESET");
return;
} else if (Passport::CountSecureSecretId(secret)
!= settings.vsecure_secret_id().v) {
LOG(("API Error: Wrong secure secret id."));
consumer.put_error("SUGGEST_SECRET_RESET");
return;
} else {
secureSecret = QByteArray(
reinterpret_cast<const char*>(secret.data()),
secret.size());
}
}
_api.request(MTPaccount_GetPassword(
)).done([=](const MTPaccount_Password &result) {
const auto latestState = ProcessMtpState(result);
sendMTPaccountUpdatePasswordSettings(
latestState,
secureSecret,
consumer);
}).fail([=](const MTP::Error &error) {
consumer.put_error_copy(error.type());
}).send();
}).fail([=](const MTP::Error &error) {
consumer.put_error_copy(error.type());
}).send();
} else {
sendMTPaccountUpdatePasswordSettings(
latestState,
QByteArray(),
consumer);
}
}).fail([=](const MTP::Error &error) {
consumer.put_error_copy(error.type());
}).send();
return rpl::lifetime();
};
}
rpl::producer<rpl::no_value, QString> CloudPassword::check(
const QString &password) {
return [=](auto consumer) {
_api.request(MTPaccount_GetPassword(
)).done([=](const MTPaccount_Password &result) {
const auto latestState = ProcessMtpState(result);
const auto input = [&] {
if (password.isEmpty()) {
return Core::CloudPasswordResult{
MTP_inputCheckPasswordEmpty()
};
}
const auto hash = Core::ComputeCloudPasswordHash(
latestState.mtp.request.algo,
bytes::make_span(password.toUtf8()));
return Core::ComputeCloudPasswordCheck(
latestState.mtp.request,
hash);
}();
_api.request(MTPaccount_GetPasswordSettings(
input.result
)).done([=](const MTPaccount_PasswordSettings &result) {
consumer.put_done();
}).fail([=](const MTP::Error &error) {
consumer.put_error_copy(error.type());
}).send();
}).fail([=](const MTP::Error &error) {
consumer.put_error_copy(error.type());
}).send();
return rpl::lifetime();
};
}
rpl::producer<rpl::no_value, QString> CloudPassword::confirmEmail(
const QString &code) {
return [=](auto consumer) {
_api.request(MTPaccount_ConfirmPasswordEmail(
MTP_string(code)
)).done([=] {
_api.request(MTPaccount_GetPassword(
)).done([=](const MTPaccount_Password &result) {
apply(ProcessMtpState(result));
consumer.put_done();
}).fail([=](const MTP::Error &error) {
consumer.put_error_copy(error.type());
}).send();
}).fail([=](const MTP::Error &error) {
consumer.put_error_copy(error.type());
}).handleFloodErrors().send();
return rpl::lifetime();
};
}
rpl::producer<rpl::no_value, QString> CloudPassword::resendEmailCode() {
return [=](auto consumer) {
_api.request(MTPaccount_ResendPasswordEmail(
)).done([=] {
_api.request(MTPaccount_GetPassword(
)).done([=](const MTPaccount_Password &result) {
apply(ProcessMtpState(result));
consumer.put_done();
}).fail([=](const MTP::Error &error) {
consumer.put_error_copy(error.type());
}).send();
}).fail([=](const MTP::Error &error) {
consumer.put_error_copy(error.type());
}).handleFloodErrors().send();
return rpl::lifetime();
};
}
rpl::producer<CloudPassword::SetOk, QString> CloudPassword::setEmail(
const QString &oldPassword,
const QString &recoveryEmail) {
const auto generatePasswordCheck = [=](
const Core::CloudPasswordState &latestState) {
if (oldPassword.isEmpty() || !latestState.hasPassword) {
return Core::CloudPasswordResult{
MTP_inputCheckPasswordEmpty()
};
}
const auto hash = Core::ComputeCloudPasswordHash(
latestState.mtp.request.algo,
bytes::make_span(oldPassword.toUtf8()));
return Core::ComputeCloudPasswordCheck(
latestState.mtp.request,
hash);
};
const auto finish = [=](auto consumer, int unconfirmedEmailLengthCode) {
_api.request(MTPaccount_GetPassword(
)).done([=](const MTPaccount_Password &result) {
apply(ProcessMtpState(result));
if (unconfirmedEmailLengthCode) {
consumer.put_next(SetOk{ unconfirmedEmailLengthCode });
} else {
consumer.put_done();
}
}).fail([=](const MTP::Error &error) {
consumer.put_error_copy(error.type());
}).handleFloodErrors().send();
};
const auto sendMTPaccountUpdatePasswordSettings = [=](
const Core::CloudPasswordState &latestState,
auto consumer) {
const auto settings = MTP_account_passwordInputSettings(
MTP_flags(MTPDaccount_passwordInputSettings::Flag::f_email),
MTP_passwordKdfAlgoUnknown(),
MTP_bytes(),
MTP_string(),
MTP_string(recoveryEmail),
MTPSecureSecretSettings());
_api.request(MTPaccount_UpdatePasswordSettings(
generatePasswordCheck(latestState).result,
settings
)).done([=] {
finish(consumer, 0);
}).fail([=](const MTP::Error &error) {
const auto &type = error.type();
const auto prefix = u"EMAIL_UNCONFIRMED_"_q;
if (type.startsWith(prefix)) {
const auto codeLength = base::StringViewMid(
type,
prefix.size()).toInt();
finish(consumer, codeLength);
} else {
consumer.put_error_copy(type);
}
}).handleFloodErrors().send();
};
return [=](auto consumer) {
_api.request(MTPaccount_GetPassword(
)).done([=](const MTPaccount_Password &result) {
const auto latestState = ProcessMtpState(result);
sendMTPaccountUpdatePasswordSettings(latestState, consumer);
}).fail([=](const MTP::Error &error) {
consumer.put_error_copy(error.type());
}).send();
return rpl::lifetime();
};
}
rpl::producer<rpl::no_value, QString> CloudPassword::recoverPassword(
const QString &code,
const QString &newPassword,
const QString &newHint) {
const auto finish = [=](auto consumer) {
_api.request(MTPaccount_GetPassword(
)).done([=](const MTPaccount_Password &result) {
apply(ProcessMtpState(result));
consumer.put_done();
}).fail([=](const MTP::Error &error) {
consumer.put_error_copy(error.type());
}).handleFloodErrors().send();
};
const auto sendMTPaccountUpdatePasswordSettings = [=](
const Core::CloudPasswordState &latestState,
auto consumer) {
const auto newPasswordBytes = newPassword.toUtf8();
const auto newPasswordHash = Core::ComputeCloudPasswordDigest(
latestState.mtp.newPassword,
bytes::make_span(newPasswordBytes));
if (!newPassword.isEmpty() && newPasswordHash.modpow.empty()) {
consumer.put_error("INTERNAL_SERVER_ERROR");
return;
}
using Flag = MTPDaccount_passwordInputSettings::Flag;
const auto flags = Flag::f_new_algo
| Flag::f_new_password_hash
| Flag::f_hint;
const auto settings = MTP_account_passwordInputSettings(
MTP_flags(flags),
Core::PrepareCloudPasswordAlgo(newPassword.isEmpty()
? v::null
: latestState.mtp.newPassword),
newPassword.isEmpty()
? MTP_bytes()
: MTP_bytes(newPasswordHash.modpow),
MTP_string(newHint),
MTP_string(),
MTPSecureSecretSettings());
_api.request(MTPauth_RecoverPassword(
MTP_flags(newPassword.isEmpty()
? MTPauth_RecoverPassword::Flags(0)
: MTPauth_RecoverPassword::Flag::f_new_settings),
MTP_string(code),
settings
)).done([=](const MTPauth_Authorization &result) {
finish(consumer);
}).fail([=](const MTP::Error &error) {
const auto &type = error.type();
consumer.put_error_copy(type);
}).handleFloodErrors().send();
};
return [=](auto consumer) {
_api.request(MTPaccount_GetPassword(
)).done([=](const MTPaccount_Password &result) {
const auto latestState = ProcessMtpState(result);
sendMTPaccountUpdatePasswordSettings(latestState, consumer);
}).fail([=](const MTP::Error &error) {
consumer.put_error_copy(error.type());
}).send();
return rpl::lifetime();
};
}
rpl::producer<QString, QString> CloudPassword::requestPasswordRecovery() {
return [=](auto consumer) {
_api.request(MTPauth_RequestPasswordRecovery(
)).done([=](const MTPauth_PasswordRecovery &result) {
result.match([&](const MTPDauth_passwordRecovery &data) {
consumer.put_next(qs(data.vemail_pattern().v));
});
consumer.put_done();
}).fail([=](const MTP::Error &error) {
consumer.put_error_copy(error.type());
}).send();
return rpl::lifetime();
};
}
auto CloudPassword::checkRecoveryEmailAddressCode(const QString &code)
-> rpl::producer<rpl::no_value, QString> {
return [=](auto consumer) {
_api.request(MTPauth_CheckRecoveryPassword(
MTP_string(code)
)).done([=] {
consumer.put_done();
}).fail([=](const MTP::Error &error) {
consumer.put_error_copy(error.type());
}).handleFloodErrors().send();
return rpl::lifetime();
};
}
void RequestLoginEmailCode(
MTP::Sender &api,
const QString &sendToEmail,
Fn<void(int length, const QString &pattern)> done,
Fn<void(const QString &error)> fail) {
api.request(MTPaccount_SendVerifyEmailCode(
MTP_emailVerifyPurposeLoginChange(),
MTP_string(sendToEmail)
)).done([=](const MTPaccount_SentEmailCode &result) {
done(result.data().vlength().v, qs(result.data().vemail_pattern()));
}).fail([=](const MTP::Error &error) {
fail(error.type());
}).send();
}
void VerifyLoginEmail(
MTP::Sender &api,
const QString &code,
Fn<void()> done,
Fn<void(const QString &error)> fail) {
api.request(MTPaccount_VerifyEmail(
MTP_emailVerifyPurposeLoginChange(),
MTP_emailVerificationCode(MTP_string(code))
)).done([=](const MTPaccount_EmailVerified &result) {
result.match([=](const MTPDaccount_emailVerified &data) {
done();
}, [=](const MTPDaccount_emailVerifiedLogin &data) {
fail(QString());
});
}).fail([=](const MTP::Error &error) {
fail(error.type());
}).send();
}
} // namespace Api

View File

@@ -0,0 +1,84 @@
/*
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
*/
#pragma once
#include "mtproto/sender.h"
namespace Core {
struct CloudPasswordState;
} // namespace Core
class ApiWrap;
namespace Main {
class Session;
} // namespace Main
namespace Api {
class CloudPassword final {
public:
struct SetOk {
int unconfirmedEmailLengthCode = 0;
};
using ResetRetryDate = int;
explicit CloudPassword(not_null<ApiWrap*> api);
void reload();
void clearUnconfirmedPassword();
rpl::producer<Core::CloudPasswordState> state() const;
std::optional<Core::CloudPasswordState> stateCurrent() const;
rpl::producer<ResetRetryDate, QString> resetPassword();
rpl::producer<rpl::no_value, QString> cancelResetPassword();
rpl::producer<SetOk, QString> set(
const QString &oldPassword,
const QString &newPassword,
const QString &hint,
bool hasRecoveryEmail,
const QString &recoveryEmail);
rpl::producer<rpl::no_value, QString> check(const QString &password);
rpl::producer<rpl::no_value, QString> confirmEmail(const QString &code);
rpl::producer<rpl::no_value, QString> resendEmailCode();
rpl::producer<SetOk, QString> setEmail(
const QString &oldPassword,
const QString &recoveryEmail);
rpl::producer<rpl::no_value, QString> recoverPassword(
const QString &code,
const QString &newPassword,
const QString &newHint);
rpl::producer<QString, QString> requestPasswordRecovery();
rpl::producer<rpl::no_value, QString> checkRecoveryEmailAddressCode(
const QString &code);
private:
void apply(Core::CloudPasswordState state);
MTP::Sender _api;
mtpRequestId _requestId = 0;
std::unique_ptr<Core::CloudPasswordState> _state;
rpl::event_stream<Core::CloudPasswordState> _stateChanges;
};
void RequestLoginEmailCode(
MTP::Sender &api,
const QString &sendToEmail,
Fn<void(int length, const QString &pattern)> done,
Fn<void(const QString &error)> fail);
void VerifyLoginEmail(
MTP::Sender &api,
const QString &code,
Fn<void()> done,
Fn<void(const QString &error)> fail);
} // namespace Api

View File

@@ -0,0 +1,48 @@
/*
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 "api/api_common.h"
#include "base/qt/qt_key_modifiers.h"
#include "data/data_histories.h"
#include "data/data_thread.h"
#include "history/history.h"
namespace Api {
MTPSuggestedPost SuggestToMTP(SuggestOptions suggest) {
using Flag = MTPDsuggestedPost::Flag;
return suggest.exists
? MTP_suggestedPost(
MTP_flags((suggest.date ? Flag::f_schedule_date : Flag())
| (suggest.price().empty() ? Flag() : Flag::f_price)),
StarsAmountToTL(suggest.price()),
MTP_int(suggest.date))
: MTPSuggestedPost();
}
SendAction::SendAction(
not_null<Data::Thread*> thread,
SendOptions options)
: history(thread->owningHistory())
, options(options)
, replyTo({ .messageId = { history->peer->id, thread->topicRootId() } }) {
replyTo.topicRootId = replyTo.messageId.msg;
}
SendOptions DefaultSendWhenOnlineOptions() {
return {
.scheduled = kScheduledUntilOnlineTimestamp,
.silent = base::IsCtrlPressed(),
};
}
MTPInputReplyTo SendAction::mtpReplyTo() const {
return Data::ReplyToForMTP(history, replyTo);
}
} // namespace Api

View File

@@ -0,0 +1,86 @@
/*
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
*/
#pragma once
#include "data/data_drafts.h"
class History;
namespace Data {
class Thread;
} // namespace Data
namespace Api {
inline constexpr auto kScheduledUntilOnlineTimestamp = TimeId(0x7FFFFFFE);
[[nodiscard]] MTPSuggestedPost SuggestToMTP(SuggestOptions suggest);
struct SendOptions {
uint64 price = 0;
PeerData *sendAs = nullptr;
TimeId scheduled = 0;
TimeId scheduleRepeatPeriod = 0;
BusinessShortcutId shortcutId = 0;
EffectId effectId = 0;
int starsApproved = 0;
bool silent = false;
bool handleSupportSwitch = false;
bool invertCaption = false;
bool hideViaBot = false;
crl::time ttlSeconds = 0;
SuggestOptions suggest;
friend inline bool operator==(
const SendOptions &,
const SendOptions &) = default;
};
[[nodiscard]] SendOptions DefaultSendWhenOnlineOptions();
enum class SendType {
Normal,
Scheduled,
ScheduledToUser, // For "Send when online".
};
struct SendAction {
explicit SendAction(
not_null<Data::Thread*> thread,
SendOptions options = SendOptions());
not_null<History*> history;
SendOptions options;
FullReplyTo replyTo;
bool clearDraft = true;
bool generateLocal = true;
MsgId replaceMediaOf = 0;
[[nodiscard]] MTPInputReplyTo mtpReplyTo() const;
friend inline bool operator==(
const SendAction &,
const SendAction &) = default;
};
struct MessageToSend {
explicit MessageToSend(SendAction action) : action(action) {
}
SendAction action;
TextWithTags textWithTags;
Data::WebPageDraft webPage;
};
struct RemoteFileInfo {
MTPInputFile file;
std::optional<MTPInputFile> thumb;
std::optional<MTPInputPhoto> videoCover;
std::vector<MTPInputDocument> attachedStickers;
};
} // namespace Api

View File

@@ -0,0 +1,172 @@
/*
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 "api/api_confirm_phone.h"
#include "apiwrap.h"
#include "lang/lang_keys.h"
#include "main/main_account.h"
#include "main/main_session.h"
#include "ui/boxes/confirm_box.h"
#include "ui/boxes/confirm_phone_box.h"
#include "ui/text/format_values.h" // Ui::FormatPhone
#include "window/window_session_controller.h"
namespace Api {
ConfirmPhone::ConfirmPhone(not_null<ApiWrap*> api)
: _api(&api->instance()) {
}
void ConfirmPhone::resolve(
not_null<Window::SessionController*> controller,
const QString &phone,
const QString &hash) {
if (_sendRequestId) {
return;
}
_sendRequestId = _api.request(MTPaccount_SendConfirmPhoneCode(
MTP_string(hash),
MTP_codeSettings(
MTP_flags(0),
MTPVector<MTPbytes>(),
MTPstring(),
MTPBool())
)).done([=](const MTPauth_SentCode &result) {
_sendRequestId = 0;
result.match([&](const MTPDauth_sentCode &data) {
const auto bad = [](const char *type) {
LOG(("API Error: Should not be '%1'.").arg(type));
return 0;
};
const auto sentCodeLength = data.vtype().match([&](
const MTPDauth_sentCodeTypeApp &data) {
LOG(("Error: should not be in-app code!"));
return 0;
}, [&](const MTPDauth_sentCodeTypeSms &data) {
return data.vlength().v;
}, [&](const MTPDauth_sentCodeTypeFragmentSms &data) {
return data.vlength().v;
}, [&](const MTPDauth_sentCodeTypeCall &data) {
return data.vlength().v;
}, [&](const MTPDauth_sentCodeTypeFlashCall &) {
return bad("FlashCall");
}, [&](const MTPDauth_sentCodeTypeMissedCall &) {
return bad("MissedCall");
}, [&](const MTPDauth_sentCodeTypeFirebaseSms &) {
return bad("FirebaseSms");
}, [&](const MTPDauth_sentCodeTypeEmailCode &) {
return bad("EmailCode");
}, [&](const MTPDauth_sentCodeTypeSmsWord &) {
return bad("SmsWord");
}, [&](const MTPDauth_sentCodeTypeSmsPhrase &) {
return bad("SmsPhrase");
}, [&](const MTPDauth_sentCodeTypeSetUpEmailRequired &) {
return bad("SetUpEmailRequired");
});
const auto fragmentUrl = data.vtype().match([](
const MTPDauth_sentCodeTypeFragmentSms &data) {
return qs(data.vurl());
}, [](const auto &) { return QString(); });
const auto phoneHash = qs(data.vphone_code_hash());
const auto timeout = [&]() -> std::optional<int> {
if (const auto nextType = data.vnext_type()) {
if (nextType->type() == mtpc_auth_codeTypeCall) {
return data.vtimeout().value_or(60);
}
}
return std::nullopt;
}();
auto box = Box<Ui::ConfirmPhoneBox>(
phone,
sentCodeLength,
fragmentUrl,
timeout);
const auto boxWeak = base::make_weak(box.data());
using LoginCode = rpl::event_stream<QString>;
const auto codeHandles = box->lifetime().make_state<LoginCode>();
controller->session().account().setHandleLoginCode([=](
const QString &code) {
codeHandles->fire_copy(code);
});
box->resendRequests(
) | rpl::on_next([=] {
_api.request(MTPauth_ResendCode(
MTP_flags(0),
MTP_string(phone),
MTP_string(phoneHash),
MTPstring() // reason
)).done([=] {
if (boxWeak) {
boxWeak->callDone();
}
}).send();
}, box->lifetime());
rpl::merge(
codeHandles->events(),
box->checkRequests()
) | rpl::on_next([=](const QString &code) {
if (_checkRequestId) {
return;
}
_checkRequestId = _api.request(MTPaccount_ConfirmPhone(
MTP_string(phoneHash),
MTP_string(code)
)).done([=] {
_checkRequestId = 0;
controller->show(
Ui::MakeInformBox(
tr::lng_confirm_phone_success(
tr::now,
lt_phone,
Ui::FormatPhone(phone))),
Ui::LayerOption::CloseOther);
}).fail([=](const MTP::Error &error) {
_checkRequestId = 0;
if (!boxWeak) {
return;
}
const auto errorText = MTP::IsFloodError(error)
? tr::lng_flood_error(tr::now)
: (error.type() == (u"PHONE_CODE_EMPTY"_q)
|| error.type() == (u"PHONE_CODE_INVALID"_q))
? tr::lng_bad_code(tr::now)
: Lang::Hard::ServerError();
boxWeak->showServerError(errorText);
}).handleFloodErrors().send();
}, box->lifetime());
box->boxClosing(
) | rpl::on_next([=] {
controller->session().account().setHandleLoginCode(nullptr);
}, box->lifetime());
controller->show(std::move(box), Ui::LayerOption::CloseOther);
}, [](const MTPDauth_sentCodeSuccess &) {
LOG(("API Error: Unexpected auth.sentCodeSuccess "
"(Api::ConfirmPhone)."));
}, [](const MTPDauth_sentCodePaymentRequired &) {
LOG(("API Error: Unexpected auth.sentCodePaymentRequired "
"(Api::ConfirmPhone)."));
});
}).fail([=](const MTP::Error &error) {
_sendRequestId = 0;
_checkRequestId = 0;
const auto errorText = MTP::IsFloodError(error)
? tr::lng_flood_error(tr::now)
: (error.code() == 400)
? tr::lng_confirm_phone_link_invalid(tr::now)
: Lang::Hard::ServerError();
controller->show(
Ui::MakeInformBox(errorText),
Ui::LayerOption::CloseOther);
}).handleFloodErrors().send();
}
} // namespace Api

View File

@@ -0,0 +1,36 @@
/*
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
*/
#pragma once
#include "mtproto/sender.h"
class ApiWrap;
namespace Window {
class SessionController;
} // namespace Window
namespace Api {
class ConfirmPhone final {
public:
explicit ConfirmPhone(not_null<ApiWrap*> api);
void resolve(
not_null<Window::SessionController*> controller,
const QString &phone,
const QString &hash);
private:
MTP::Sender _api;
mtpRequestId _sendRequestId = 0;
mtpRequestId _checkRequestId = 0;
};
} // namespace Api

View File

@@ -0,0 +1,415 @@
/*
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 "api/api_credits.h"
#include "api/api_credits_history_entry.h"
#include "api/api_premium.h"
#include "api/api_statistics_data_deserialize.h"
#include "api/api_updates.h"
#include "apiwrap.h"
#include "base/unixtime.h"
#include "data/components/credits.h"
#include "data/data_channel.h"
#include "data/data_document.h"
#include "data/data_peer.h"
#include "data/data_photo.h"
#include "data/data_session.h"
#include "data/data_user.h"
#include "main/main_app_config.h"
#include "main/main_session.h"
namespace Api {
namespace {
constexpr auto kTransactionsLimit = 100;
[[nodiscard]] Data::SubscriptionEntry SubscriptionFromTL(
const MTPStarsSubscription &tl,
not_null<PeerData*> peer) {
return Data::SubscriptionEntry{
.id = qs(tl.data().vid()),
.inviteHash = qs(tl.data().vchat_invite_hash().value_or_empty()),
.title = qs(tl.data().vtitle().value_or_empty()),
.slug = qs(tl.data().vinvoice_slug().value_or_empty()),
.until = base::unixtime::parse(tl.data().vuntil_date().v),
.subscription = Data::PeerSubscription{
.credits = tl.data().vpricing().data().vamount().v,
.period = tl.data().vpricing().data().vperiod().v,
},
.barePeerId = peerFromMTP(tl.data().vpeer()).value,
.photoId = (tl.data().vphoto()
? peer->owner().photoFromWeb(
*tl.data().vphoto(),
ImageLocation())->id
: 0),
.cancelled = tl.data().is_canceled(),
.cancelledByBot = tl.data().is_bot_canceled(),
.expired = (base::unixtime::now() > tl.data().vuntil_date().v),
.canRefulfill = tl.data().is_can_refulfill(),
};
}
[[nodiscard]] Data::CreditsStatusSlice StatusFromTL(
const MTPpayments_StarsStatus &status,
not_null<PeerData*> peer) {
const auto &data = status.data();
peer->owner().processUsers(data.vusers());
peer->owner().processChats(data.vchats());
auto entries = std::vector<Data::CreditsHistoryEntry>();
if (const auto history = data.vhistory()) {
entries.reserve(history->v.size());
for (const auto &tl : history->v) {
entries.push_back(CreditsHistoryEntryFromTL(tl, peer));
}
}
auto subscriptions = std::vector<Data::SubscriptionEntry>();
if (const auto history = data.vsubscriptions()) {
subscriptions.reserve(history->v.size());
for (const auto &tl : history->v) {
subscriptions.push_back(SubscriptionFromTL(tl, peer));
}
}
return Data::CreditsStatusSlice{
.list = std::move(entries),
.subscriptions = std::move(subscriptions),
.balance = CreditsAmountFromTL(status.data().vbalance()),
.subscriptionsMissingBalance
= status.data().vsubscriptions_missing_balance().value_or_empty(),
.allLoaded = !status.data().vnext_offset().has_value()
&& !status.data().vsubscriptions_next_offset().has_value(),
.token = qs(status.data().vnext_offset().value_or_empty()),
.tokenSubscriptions = qs(
status.data().vsubscriptions_next_offset().value_or_empty()),
};
}
} // namespace
CreditsTopupOptions::CreditsTopupOptions(not_null<PeerData*> peer)
: _peer(peer)
, _api(&peer->session().api().instance()) {
}
rpl::producer<rpl::no_value, QString> CreditsTopupOptions::request() {
return [=](auto consumer) {
auto lifetime = rpl::lifetime();
const auto giftBarePeerId = !_peer->isSelf() ? _peer->id.value : 0;
const auto optionsFromTL = [giftBarePeerId](const auto &options) {
return ranges::views::all(
options
) | ranges::views::transform([=](const auto &option) {
return Data::CreditTopupOption{
.credits = option.data().vstars().v,
.product = qs(
option.data().vstore_product().value_or_empty()),
.currency = qs(option.data().vcurrency()),
.amount = option.data().vamount().v,
.extended = option.data().is_extended(),
.giftBarePeerId = giftBarePeerId,
};
}) | ranges::to_vector;
};
const auto fail = [=](const MTP::Error &error) {
consumer.put_error_copy(error.type());
};
if (_peer->isSelf()) {
using TLOption = MTPStarsTopupOption;
_api.request(MTPpayments_GetStarsTopupOptions(
)).done([=](const MTPVector<TLOption> &result) {
_options = optionsFromTL(result.v);
consumer.put_done();
}).fail(fail).send();
} else if (const auto user = _peer->asUser()) {
using TLOption = MTPStarsGiftOption;
_api.request(MTPpayments_GetStarsGiftOptions(
MTP_flags(MTPpayments_GetStarsGiftOptions::Flag::f_user_id),
user->inputUser()
)).done([=](const MTPVector<TLOption> &result) {
_options = optionsFromTL(result.v);
consumer.put_done();
}).fail(fail).send();
}
return lifetime;
};
}
Data::CreditTopupOptions CreditsTopupOptions::options() const {
return _options;
}
CreditsStatus::CreditsStatus(not_null<PeerData*> peer)
: _peer(peer)
, _api(&peer->session().api().instance()) {
}
void CreditsStatus::request(
const Data::CreditsStatusSlice::OffsetToken &token,
Fn<void(Data::CreditsStatusSlice)> done) {
if (_requestId) {
return;
}
using TLResult = MTPpayments_StarsStatus;
_requestId = _api.request(MTPpayments_GetStarsStatus(
MTP_flags(0),
_peer->isSelf() ? MTP_inputPeerSelf() : _peer->input()
)).done([=](const TLResult &result) {
_requestId = 0;
const auto &balance = result.data().vbalance();
_peer->session().credits().apply(
_peer->id,
CreditsAmountFromTL(balance));
if (const auto onstack = done) {
onstack(StatusFromTL(result, _peer));
}
}).fail([=] {
_requestId = 0;
if (const auto onstack = done) {
onstack({});
}
}).send();
}
CreditsHistory::CreditsHistory(
not_null<PeerData*> peer,
bool in,
bool out,
bool currency)
: _peer(peer)
, _flags(((in == out)
? HistoryTL::Flags(0)
: HistoryTL::Flags(0)
| (in ? HistoryTL::Flag::f_inbound : HistoryTL::Flags(0))
| (out ? HistoryTL::Flag::f_outbound : HistoryTL::Flags(0)))
| (currency ? HistoryTL::Flag::f_ton : HistoryTL::Flags(0)))
, _api(&peer->session().api().instance()) {
}
void CreditsHistory::request(
const Data::CreditsStatusSlice::OffsetToken &token,
Fn<void(Data::CreditsStatusSlice)> done) {
if (_requestId) {
return;
}
_requestId = _api.request(MTPpayments_GetStarsTransactions(
MTP_flags(_flags),
MTPstring(), // subscription_id
_peer->isSelf() ? MTP_inputPeerSelf() : _peer->input(),
MTP_string(token),
MTP_int(kTransactionsLimit)
)).done([=](const MTPpayments_StarsStatus &result) {
_requestId = 0;
done(StatusFromTL(result, _peer));
}).fail([=] {
_requestId = 0;
done({});
}).send();
}
void CreditsHistory::requestSubscriptions(
const Data::CreditsStatusSlice::OffsetToken &token,
Fn<void(Data::CreditsStatusSlice)> done,
bool missingBalance) {
if (_requestId) {
return;
}
_requestId = _api.request(MTPpayments_GetStarsSubscriptions(
MTP_flags(missingBalance
? MTPpayments_getStarsSubscriptions::Flag::f_missing_balance
: MTPpayments_getStarsSubscriptions::Flags(0)),
_peer->isSelf() ? MTP_inputPeerSelf() : _peer->input(),
MTP_string(token)
)).done([=](const MTPpayments_StarsStatus &result) {
_requestId = 0;
done(StatusFromTL(result, _peer));
}).fail([=] {
_requestId = 0;
done({});
}).send();
}
rpl::producer<not_null<PeerData*>> PremiumPeerBot(
not_null<Main::Session*> session) {
const auto username = session->appConfig().get<QString>(
u"premium_bot_username"_q,
QString());
if (username.isEmpty()) {
return rpl::never<not_null<PeerData*>>();
}
if (const auto p = session->data().peerByUsername(username)) {
return rpl::single<not_null<PeerData*>>(p);
}
return [=](auto consumer) {
auto lifetime = rpl::lifetime();
const auto api = lifetime.make_state<MTP::Sender>(&session->mtp());
api->request(MTPcontacts_ResolveUsername(
MTP_flags(0),
MTP_string(username),
MTP_string()
)).done([=](const MTPcontacts_ResolvedPeer &result) {
session->data().processUsers(result.data().vusers());
session->data().processChats(result.data().vchats());
const auto botPeer = session->data().peerLoaded(
peerFromMTP(result.data().vpeer()));
if (!botPeer) {
return consumer.put_done();
}
consumer.put_next(not_null{ botPeer });
}).send();
return lifetime;
};
}
CreditsEarnStatistics::CreditsEarnStatistics(not_null<PeerData*> peer)
: StatisticsRequestSender(peer)
, _isUser(peer->isUser()) {
}
rpl::producer<rpl::no_value, QString> CreditsEarnStatistics::request() {
return [=](auto consumer) {
auto lifetime = rpl::lifetime();
const auto finish = [=](const QString &url) {
api().request(MTPpayments_GetStarsRevenueStats(
MTP_flags(0),
(_isUser ? user()->input() : channel()->input())
)).done([=](const MTPpayments_StarsRevenueStats &result) {
const auto &data = result.data();
const auto &status = data.vstatus().data();
_data = Data::CreditsEarnStatistics{
.revenueGraph = StatisticalGraphFromTL(
data.vrevenue_graph()),
.currentBalance = CreditsAmountFromTL(
status.vcurrent_balance()),
.availableBalance = CreditsAmountFromTL(
status.vavailable_balance()),
.overallRevenue = CreditsAmountFromTL(
status.voverall_revenue()),
.usdRate = data.vusd_rate().v,
.isWithdrawalEnabled = status.is_withdrawal_enabled(),
.nextWithdrawalAt = status.vnext_withdrawal_at()
? base::unixtime::parse(
status.vnext_withdrawal_at()->v)
: QDateTime(),
.buyAdsUrl = url,
};
consumer.put_done();
}).fail([=](const MTP::Error &error) {
consumer.put_error_copy(error.type());
}).send();
};
api().request(
MTPpayments_GetStarsRevenueAdsAccountUrl(
(_isUser ? user()->input() : channel()->input()))
).done([=](const MTPpayments_StarsRevenueAdsAccountUrl &result) {
finish(qs(result.data().vurl()));
}).fail([=](const MTP::Error &error) {
finish({});
}).send();
return lifetime;
};
}
Data::CreditsEarnStatistics CreditsEarnStatistics::data() const {
return _data;
}
CreditsGiveawayOptions::CreditsGiveawayOptions(not_null<PeerData*> peer)
: _peer(peer)
, _api(&peer->session().api().instance()) {
}
rpl::producer<rpl::no_value, QString> CreditsGiveawayOptions::request() {
return [=](auto consumer) {
auto lifetime = rpl::lifetime();
using TLOption = MTPStarsGiveawayOption;
const auto optionsFromTL = [=](const auto &options) {
return ranges::views::all(
options
) | ranges::views::transform([=](const auto &option) {
return Data::CreditsGiveawayOption{
.winners = ranges::views::all(
option.data().vwinners().v
) | ranges::views::transform([](const auto &winner) {
return Data::CreditsGiveawayOption::Winner{
.users = winner.data().vusers().v,
.perUserStars = winner.data().vper_user_stars().v,
.isDefault = winner.data().is_default(),
};
}) | ranges::to_vector,
.storeProduct = qs(
option.data().vstore_product().value_or_empty()),
.currency = qs(option.data().vcurrency()),
.amount = option.data().vamount().v,
.credits = option.data().vstars().v,
.yearlyBoosts = option.data().vyearly_boosts().v,
.isExtended = option.data().is_extended(),
.isDefault = option.data().is_default(),
};
}) | ranges::to_vector;
};
const auto fail = [=](const MTP::Error &error) {
consumer.put_error_copy(error.type());
};
_api.request(MTPpayments_GetStarsGiveawayOptions(
)).done([=](const MTPVector<TLOption> &result) {
_options = optionsFromTL(result.v);
consumer.put_done();
}).fail(fail).send();
return lifetime;
};
}
Data::CreditsGiveawayOptions CreditsGiveawayOptions::options() const {
return _options;
}
void EditCreditsSubscription(
not_null<Main::Session*> session,
const QString &id,
bool cancel,
Fn<void()> done,
Fn<void(QString)> fail) {
using Flag = MTPpayments_ChangeStarsSubscription::Flag;
session->api().request(
MTPpayments_ChangeStarsSubscription(
MTP_flags(Flag::f_canceled),
MTP_inputPeerSelf(),
MTP_string(id),
MTP_bool(cancel)
)).done(done).fail([=](const MTP::Error &e) { fail(e.type()); }).send();
}
MTPInputSavedStarGift InputSavedStarGiftId(
const Data::SavedStarGiftId &id,
const std::shared_ptr<Data::UniqueGift> &unique) {
return (!id && unique)
? MTP_inputSavedStarGiftSlug(MTP_string(unique->slug))
: id.isUser()
? MTP_inputSavedStarGiftUser(MTP_int(id.userMessageId().bare))
: MTP_inputSavedStarGiftChat(
id.chat()->input(),
MTP_long(id.chatSavedId()));
}
} // namespace Api

View File

@@ -0,0 +1,132 @@
/*
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
*/
#pragma once
#include "api/api_statistics_sender.h"
#include "data/data_credits.h"
#include "data/data_credits_earn.h"
#include "mtproto/sender.h"
namespace Data {
class SavedStarGiftId;
} // namespace Data
namespace Main {
class Session;
} // namespace Main
class UserData;
namespace Api {
class CreditsTopupOptions final {
public:
CreditsTopupOptions(not_null<PeerData*> peer);
[[nodiscard]] rpl::producer<rpl::no_value, QString> request();
[[nodiscard]] Data::CreditTopupOptions options() const;
private:
const not_null<PeerData*> _peer;
Data::CreditTopupOptions _options;
MTP::Sender _api;
};
class CreditsGiveawayOptions final {
public:
CreditsGiveawayOptions(not_null<PeerData*> peer);
[[nodiscard]] rpl::producer<rpl::no_value, QString> request();
[[nodiscard]] Data::CreditsGiveawayOptions options() const;
private:
const not_null<PeerData*> _peer;
Data::CreditsGiveawayOptions _options;
MTP::Sender _api;
};
class CreditsStatus final {
public:
CreditsStatus(not_null<PeerData*> peer);
void request(
const Data::CreditsStatusSlice::OffsetToken &token,
Fn<void(Data::CreditsStatusSlice)> done);
private:
const not_null<PeerData*> _peer;
mtpRequestId _requestId = 0;
MTP::Sender _api;
};
class CreditsHistory final {
public:
CreditsHistory(
not_null<PeerData*> peer,
bool in,
bool out,
bool currency = false);
void request(
const Data::CreditsStatusSlice::OffsetToken &token,
Fn<void(Data::CreditsStatusSlice)> done);
void requestSubscriptions(
const Data::CreditsStatusSlice::OffsetToken &token,
Fn<void(Data::CreditsStatusSlice)> done,
bool missingBalance = false);
private:
using HistoryTL = MTPpayments_GetStarsTransactions;
const not_null<PeerData*> _peer;
const HistoryTL::Flags _flags;
mtpRequestId _requestId = 0;
MTP::Sender _api;
};
class CreditsEarnStatistics final : public StatisticsRequestSender {
public:
explicit CreditsEarnStatistics(not_null<PeerData*>);
[[nodiscard]] rpl::producer<rpl::no_value, QString> request();
[[nodiscard]] Data::CreditsEarnStatistics data() const;
private:
const bool _isUser = false;
Data::CreditsEarnStatistics _data;
mtpRequestId _requestId = 0;
};
[[nodiscard]] rpl::producer<not_null<PeerData*>> PremiumPeerBot(
not_null<Main::Session*> session);
void EditCreditsSubscription(
not_null<Main::Session*> session,
const QString &id,
bool cancel,
Fn<void()> done,
Fn<void(QString)> fail);
[[nodiscard]] MTPInputSavedStarGift InputSavedStarGiftId(
const Data::SavedStarGiftId &id,
const std::shared_ptr<Data::UniqueGift> &unique = nullptr);
} // namespace Api

View File

@@ -0,0 +1,170 @@
/*
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 "api/api_credits_history_entry.h"
#include "api/api_premium.h"
#include "base/unixtime.h"
#include "data/data_channel.h"
#include "data/data_credits.h"
#include "data/data_document.h"
#include "data/data_peer.h"
#include "data/data_photo.h"
#include "data/data_session.h"
#include "data/data_user.h"
#include "main/main_session.h"
namespace Api {
Data::CreditsHistoryEntry CreditsHistoryEntryFromTL(
const MTPStarsTransaction &tl,
not_null<PeerData*> peer) {
using HistoryPeerTL = MTPDstarsTransactionPeer;
using namespace Data;
const auto owner = &peer->owner();
const auto photo = tl.data().vphoto()
? owner->photoFromWeb(*tl.data().vphoto(), ImageLocation())
: nullptr;
auto extended = std::vector<CreditsHistoryMedia>();
if (const auto list = tl.data().vextended_media()) {
extended.reserve(list->v.size());
for (const auto &media : list->v) {
media.match([&](const MTPDmessageMediaPhoto &data) {
if (const auto inner = data.vphoto()) {
const auto photo = owner->processPhoto(*inner);
if (!photo->isNull()) {
extended.push_back(CreditsHistoryMedia{
.type = CreditsHistoryMediaType::Photo,
.id = photo->id,
});
}
}
}, [&](const MTPDmessageMediaDocument &data) {
if (const auto inner = data.vdocument()) {
const auto document = owner->processDocument(
*inner,
data.valt_documents());
if (document->isAnimation()
|| document->isVideoFile()
|| document->isGifv()) {
extended.push_back(CreditsHistoryMedia{
.type = CreditsHistoryMediaType::Video,
.id = document->id,
});
}
}
}, [&](const auto &) {});
}
}
const auto barePeerId = tl.data().vpeer().match([](
const HistoryPeerTL &p) {
return peerFromMTP(p.vpeer());
}, [](const auto &) {
return PeerId(0);
}).value;
const auto stargift = tl.data().vstargift();
const auto nonUniqueGift = stargift
? stargift->match([&](const MTPDstarGift &data) {
return &data;
}, [](const auto &) { return (const MTPDstarGift*)nullptr; })
: nullptr;
const auto reaction = tl.data().is_reaction();
const auto amount = CreditsAmountFromTL(tl.data().vamount());
const auto starrefAmount = CreditsAmountFromTL(
tl.data().vstarref_amount());
const auto starrefCommission
= tl.data().vstarref_commission_permille().value_or_empty();
const auto starrefBarePeerId = tl.data().vstarref_peer()
? peerFromMTP(*tl.data().vstarref_peer()).value
: 0;
const auto incoming = (amount >= CreditsAmount());
const auto paidMessagesCount
= tl.data().vpaid_messages().value_or_empty();
const auto premiumMonthsForStars
= tl.data().vpremium_gift_months().value_or_empty();
const auto saveActorId = (reaction
|| !extended.empty()
|| paidMessagesCount) && incoming;
const auto parsedGift = stargift
? FromTL(&peer->session(), *stargift)
: std::optional<Data::StarGift>();
const auto giftStickerId = parsedGift ? parsedGift->document->id : 0;
return Data::CreditsHistoryEntry{
.id = qs(tl.data().vid()),
.title = qs(tl.data().vtitle().value_or_empty()),
.description = { qs(tl.data().vdescription().value_or_empty()) },
.date = base::unixtime::parse(
tl.data().vads_proceeds_from_date().value_or(
tl.data().vdate().v)),
.photoId = photo ? photo->id : 0,
.extended = std::move(extended),
.credits = CreditsAmountFromTL(tl.data().vamount()),
.bareMsgId = uint64(tl.data().vmsg_id().value_or_empty()),
.barePeerId = saveActorId ? peer->id.value : barePeerId,
.bareGiveawayMsgId = uint64(
tl.data().vgiveaway_post_id().value_or_empty()),
.bareGiftStickerId = giftStickerId,
.bareActorId = saveActorId ? barePeerId : uint64(0),
.uniqueGift = parsedGift ? parsedGift->unique : nullptr,
.starrefAmount = paidMessagesCount ? CreditsAmount() : starrefAmount,
.starrefCommission = paidMessagesCount ? 0 : starrefCommission,
.starrefRecipientId = paidMessagesCount ? 0 : starrefBarePeerId,
.peerType = tl.data().vpeer().match([](const HistoryPeerTL &) {
return Data::CreditsHistoryEntry::PeerType::Peer;
}, [](const MTPDstarsTransactionPeerPlayMarket &) {
return Data::CreditsHistoryEntry::PeerType::PlayMarket;
}, [](const MTPDstarsTransactionPeerFragment &) {
return Data::CreditsHistoryEntry::PeerType::Fragment;
}, [](const MTPDstarsTransactionPeerAppStore &) {
return Data::CreditsHistoryEntry::PeerType::AppStore;
}, [](const MTPDstarsTransactionPeerUnsupported &) {
return Data::CreditsHistoryEntry::PeerType::Unsupported;
}, [](const MTPDstarsTransactionPeerPremiumBot &) {
return Data::CreditsHistoryEntry::PeerType::PremiumBot;
}, [](const MTPDstarsTransactionPeerAds &) {
return Data::CreditsHistoryEntry::PeerType::Ads;
}, [](const MTPDstarsTransactionPeerAPI &) {
return Data::CreditsHistoryEntry::PeerType::API;
}),
.subscriptionUntil = tl.data().vsubscription_period()
? base::unixtime::parse(base::unixtime::now()
+ tl.data().vsubscription_period()->v)
: QDateTime(),
.adsProceedsToDate = tl.data().vads_proceeds_to_date()
? base::unixtime::parse(tl.data().vads_proceeds_to_date()->v)
: QDateTime(),
.successDate = tl.data().vtransaction_date()
? base::unixtime::parse(tl.data().vtransaction_date()->v)
: QDateTime(),
.successLink = qs(tl.data().vtransaction_url().value_or_empty()),
.paidMessagesCount = paidMessagesCount,
.paidMessagesAmount = (paidMessagesCount
? starrefAmount
: CreditsAmount()),
.paidMessagesCommission = paidMessagesCount ? starrefCommission : 0,
.limitedCount = parsedGift ? parsedGift->limitedCount : 0,
.limitedLeft = parsedGift ? parsedGift->limitedLeft : 0,
.starsConverted = int(nonUniqueGift
? nonUniqueGift->vconvert_stars().v
: 0),
.premiumMonthsForStars = premiumMonthsForStars,
.floodSkip = int(tl.data().vfloodskip_number().value_or(0)),
.converted = stargift && incoming,
.stargift = stargift.has_value(),
.postsSearch = tl.data().is_posts_search(),
.giftUpgraded = tl.data().is_stargift_upgrade(),
.giftResale = tl.data().is_stargift_resale(),
.reaction = tl.data().is_reaction(),
.refunded = tl.data().is_refund(),
.pending = tl.data().is_pending(),
.failed = tl.data().is_failed(),
.in = incoming,
.gift = tl.data().is_gift() || stargift.has_value(),
};
}
} // namespace Api

View File

@@ -0,0 +1,22 @@
/*
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
*/
#pragma once
class PeerData;
namespace Data {
struct CreditsHistoryEntry;
} // namespace Data
namespace Api {
[[nodiscard]] Data::CreditsHistoryEntry CreditsHistoryEntryFromTL(
const MTPStarsTransaction &tl,
not_null<PeerData*> peer);
} // namespace Api

View File

@@ -0,0 +1,159 @@
/*
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 "api/api_earn.h"
#include "api/api_cloud_password.h"
#include "apiwrap.h"
#include "ui/layers/generic_box.h"
#include "boxes/passcode_box.h"
#include "data/data_channel.h"
#include "data/data_session.h"
#include "lang/lang_keys.h"
#include "main/main_session.h"
#include "ui/basic_click_handlers.h"
#include "ui/widgets/buttons.h"
namespace Api {
void RestrictSponsored(
not_null<ChannelData*> channel,
bool restricted,
Fn<void(QString)> failed) {
channel->session().api().request(MTPchannels_RestrictSponsoredMessages(
channel->inputChannel(),
MTP_bool(restricted))
).done([=](const MTPUpdates &updates) {
channel->session().api().applyUpdates(updates);
}).fail([=](const MTP::Error &error) {
failed(error.type());
}).send();
}
void HandleWithdrawalButton(
RewardReceiver receiver,
not_null<Ui::RippleButton*> button,
std::shared_ptr<Ui::Show> show) {
Expects(receiver.currencyReceiver
|| (receiver.creditsReceiver && receiver.creditsAmount));
struct State {
rpl::lifetime lifetime;
bool loading = false;
};
const auto currencyReceiver = receiver.currencyReceiver;
const auto creditsReceiver = receiver.creditsReceiver;
const auto isChannel = receiver.currencyReceiver
&& receiver.currencyReceiver->isChannel();
const auto state = button->lifetime().make_state<State>();
const auto session = (currencyReceiver
? &currencyReceiver->session()
: &creditsReceiver->session());
using CreditsOutUrl = MTPpayments_StarsRevenueWithdrawalUrl;
session->api().cloudPassword().reload();
const auto processOut = [=] {
if (state->loading) {
return;
} else if (creditsReceiver && !receiver.creditsAmount()) {
return;
}
state->loading = true;
state->lifetime = session->api().cloudPassword().state(
) | rpl::take(
1
) | rpl::on_next([=](const Core::CloudPasswordState &pass) {
state->loading = false;
auto fields = PasscodeBox::CloudFields::From(pass);
fields.customTitle = isChannel
? tr::lng_channel_earn_balance_password_title()
: tr::lng_bot_earn_balance_password_title();
fields.customDescription = isChannel
? tr::lng_channel_earn_balance_password_description(tr::now)
: tr::lng_bot_earn_balance_password_description(tr::now);
fields.customSubmitButton = tr::lng_passcode_submit();
fields.customCheckCallback = crl::guard(button, [=](
const Core::CloudPasswordResult &result,
base::weak_qptr<PasscodeBox> box) {
const auto done = [=](const QString &result) {
if (!result.isEmpty()) {
UrlClickHandler::Open(result);
if (box) {
box->closeBox();
}
}
};
const auto fail = [=](const MTP::Error &error) {
const auto message = error.type();
if (box && !box->handleCustomCheckError(message)) {
show->showToast(message);
}
};
if (currencyReceiver || creditsReceiver) {
using F = MTPpayments_getStarsRevenueWithdrawalUrl::Flag;
session->api().request(
MTPpayments_GetStarsRevenueWithdrawalUrl(
MTP_flags(currencyReceiver
? F::f_ton
: F::f_amount),
currencyReceiver
? currencyReceiver->input()
: creditsReceiver->input(),
MTP_long(creditsReceiver
? receiver.creditsAmount()
: 0),
result.result
)).done([=](const CreditsOutUrl &r) {
done(qs(r.data().vurl()));
}).fail(fail).send();
}
});
show->show(Box<PasscodeBox>(session, fields));
});
};
button->setClickedCallback([=] {
if (state->loading) {
return;
}
const auto fail = [=](const MTP::Error &error) {
auto box = PrePasswordErrorBox(
error.type(),
session,
TextWithEntities{
tr::lng_channel_earn_out_check_password_about(tr::now),
});
if (box) {
show->show(std::move(box));
state->loading = false;
} else {
processOut();
}
};
if (currencyReceiver || creditsReceiver) {
using F = MTPpayments_getStarsRevenueWithdrawalUrl::Flag;
session->api().request(
MTPpayments_GetStarsRevenueWithdrawalUrl(
MTP_flags(currencyReceiver
? F::f_ton
: F::f_amount),
currencyReceiver
? currencyReceiver->input()
: creditsReceiver->input(),
MTP_long(creditsReceiver
? receiver.creditsAmount()
: 0),
MTP_inputCheckPasswordEmpty()
)).fail(fail).send();
}
});
}
} // namespace Api

View File

@@ -0,0 +1,35 @@
/*
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
*/
#pragma once
class ChannelData;
namespace Ui {
class RippleButton;
class Show;
} // namespace Ui
namespace Api {
void RestrictSponsored(
not_null<ChannelData*> channel,
bool restricted,
Fn<void(QString)> failed);
struct RewardReceiver final {
PeerData *currencyReceiver = nullptr;
PeerData *creditsReceiver = nullptr;
Fn<uint64()> creditsAmount;
};
void HandleWithdrawalButton(
RewardReceiver receiver,
not_null<Ui::RippleButton*> button,
std::shared_ptr<Ui::Show> show);
} // namespace Api

View File

@@ -0,0 +1,587 @@
/*
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 "api/api_editing.h"
#include "apiwrap.h"
#include "api/api_media.h"
#include "api/api_text_entities.h"
#include "base/random.h"
#include "ui/boxes/confirm_box.h"
#include "data/business/data_shortcut_messages.h"
#include "data/components/scheduled_messages.h"
#include "data/data_file_origin.h"
#include "data/data_histories.h"
#include "data/data_saved_sublist.h"
#include "data/data_session.h"
#include "data/data_todo_list.h"
#include "data/data_web_page.h"
#include "history/view/controls/history_view_compose_media_edit_manager.h"
#include "history/history.h"
#include "history/history_item_components.h"
#include "lang/lang_keys.h"
#include "main/main_session.h"
#include "mtproto/mtproto_response.h"
#include "boxes/abstract_box.h" // Ui::show().
namespace Api {
namespace {
using namespace rpl::details;
template <typename T>
constexpr auto WithId
= is_callable_plain_v<T, Fn<void()>, mtpRequestId>;
template <typename T>
constexpr auto WithoutId
= is_callable_plain_v<T, Fn<void()>>;
template <typename T>
constexpr auto WithoutCallback
= is_callable_plain_v<T>;
template <typename T>
constexpr auto ErrorWithId
= is_callable_plain_v<T, QString, mtpRequestId>;
template <typename T>
constexpr auto ErrorWithoutId
= is_callable_plain_v<T, QString>;
template <typename DoneCallback, typename FailCallback>
mtpRequestId SuggestMessage(
not_null<HistoryItem*> item,
const TextWithEntities &textWithEntities,
Data::WebPageDraft webpage,
SendOptions options,
DoneCallback &&done,
FailCallback &&fail) {
Expects(options.suggest.exists);
Expects(!options.scheduled);
const auto session = &item->history()->session();
const auto api = &session->api();
const auto thread = item->history()->amMonoforumAdmin()
? item->savedSublist()
: (Data::Thread*)item->history();
auto action = SendAction(thread, options);
action.replyTo = FullReplyTo{
.messageId = item->fullId(),
.monoforumPeerId = (item->history()->amMonoforumAdmin()
? item->sublistPeerId()
: PeerId()),
};
auto message = MessageToSend(std::move(action));
message.textWithTags = TextWithTags{
textWithEntities.text,
TextUtilities::ConvertEntitiesToTextTags(textWithEntities.entities)
};
message.webPage = webpage;
api->sendMessage(std::move(message));
const auto requestId = -1;
crl::on_main(session, [=] {
const auto type = u"MESSAGE_NOT_MODIFIED"_q;
if constexpr (ErrorWithId<FailCallback>) {
fail(type, requestId);
} else if constexpr (ErrorWithoutId<FailCallback>) {
fail(type);
} else if constexpr (WithoutCallback<FailCallback>) {
fail();
} else {
t_bad_callback(fail);
}
});
return requestId;
}
template <typename DoneCallback, typename FailCallback>
mtpRequestId SuggestMedia(
not_null<HistoryItem*> item,
const TextWithEntities &textWithEntities,
Data::WebPageDraft webpage,
SendOptions options,
DoneCallback &&done,
FailCallback &&fail,
std::optional<MTPInputMedia> inputMedia) {
Expects(options.suggest.exists);
Expects(!options.scheduled);
const auto session = &item->history()->session();
const auto api = &session->api();
const auto text = textWithEntities.text;
const auto sentEntities = EntitiesToMTP(
session,
textWithEntities.entities,
ConvertOption::SkipLocal);
const auto updateRecentStickers = inputMedia
? Api::HasAttachedStickers(*inputMedia)
: false;
const auto emptyFlag = MTPmessages_SendMedia::Flag(0);
auto replyTo = FullReplyTo{
.messageId = item->fullId(),
.monoforumPeerId = (item->history()->amMonoforumAdmin()
? item->sublistPeerId()
: PeerId()),
};
const auto flags = emptyFlag
| MTPmessages_SendMedia::Flag::f_reply_to
| MTPmessages_SendMedia::Flag::f_suggested_post
| (((!webpage.removed && !webpage.url.isEmpty() && webpage.invert)
|| options.invertCaption)
? MTPmessages_SendMedia::Flag::f_invert_media
: emptyFlag)
| (!sentEntities.v.isEmpty()
? MTPmessages_SendMedia::Flag::f_entities
: emptyFlag)
| (options.starsApproved
? MTPmessages_SendMedia::Flag::f_allow_paid_stars
: emptyFlag);
const auto randomId = base::RandomValue<uint64>();
return api->request(MTPmessages_SendMedia(
MTP_flags(flags),
item->history()->peer->input(),
ReplyToForMTP(item->history(), replyTo),
inputMedia.value_or(Data::WebPageForMTP(webpage, text.isEmpty())),
MTP_string(text),
MTP_long(randomId),
MTPReplyMarkup(),
sentEntities,
MTPint(), // schedule_date
MTPint(), // schedule_repeat_period
MTPInputPeer(), // send_as
MTPInputQuickReplyShortcut(), // quick_reply_shortcut
MTPlong(), // effect
MTP_long(options.starsApproved),
Api::SuggestToMTP(options.suggest)
)).done([=](
const MTPUpdates &result,
[[maybe_unused]] mtpRequestId requestId) {
const auto apply = [=] { api->applyUpdates(result); };
if constexpr (WithId<DoneCallback>) {
done(apply, requestId);
} else if constexpr (WithoutId<DoneCallback>) {
done(apply);
} else if constexpr (WithoutCallback<DoneCallback>) {
done();
apply();
} else {
t_bad_callback(done);
}
if (updateRecentStickers) {
api->requestSpecialStickersForce(false, false, true);
}
}).fail([=](const MTP::Error &error, mtpRequestId requestId) {
if constexpr (ErrorWithId<FailCallback>) {
fail(error.type(), requestId);
} else if constexpr (ErrorWithoutId<FailCallback>) {
fail(error.type());
} else if constexpr (WithoutCallback<FailCallback>) {
fail();
} else {
t_bad_callback(fail);
}
}).send();
}
template <typename DoneCallback, typename FailCallback>
mtpRequestId SuggestMessageOrMedia(
not_null<HistoryItem*> item,
const TextWithEntities &textWithEntities,
Data::WebPageDraft webpage,
SendOptions options,
DoneCallback &&done,
FailCallback &&fail,
std::optional<MTPInputMedia> inputMedia) {
const auto wasMedia = item->media();
if (!inputMedia && wasMedia && wasMedia->allowsEditCaption()) {
if (const auto photo = wasMedia->photo()) {
inputMedia = MTP_inputMediaPhoto(
MTP_flags(0),
photo->mtpInput(),
MTPint()); // ttl_seconds
} else if (const auto document = wasMedia->document()) {
inputMedia = MTP_inputMediaDocument(
MTP_flags(0),
document->mtpInput(),
MTPInputPhoto(), // video_cover
MTPint(), // video_timestamp
MTPint(), // ttl_seconds
MTPstring()); // query
}
}
if (inputMedia) {
return SuggestMedia(
item,
textWithEntities,
webpage,
options,
std::move(done),
std::move(fail),
inputMedia);
}
return SuggestMessage(
item,
textWithEntities,
webpage,
options,
std::move(done),
std::move(fail));
}
template <typename DoneCallback, typename FailCallback>
mtpRequestId EditMessage(
not_null<HistoryItem*> item,
const TextWithEntities &textWithEntities,
Data::WebPageDraft webpage,
SendOptions options,
DoneCallback &&done,
FailCallback &&fail,
std::optional<MTPInputMedia> inputMedia = std::nullopt) {
if (item->computeSuggestionActions()
== SuggestionActions::AcceptAndDecline) {
return SuggestMessageOrMedia(
item,
textWithEntities,
webpage,
options,
std::move(done),
std::move(fail),
inputMedia);
}
const auto session = &item->history()->session();
const auto api = &session->api();
const auto text = textWithEntities.text;
const auto sentEntities = EntitiesToMTP(
session,
textWithEntities.entities,
ConvertOption::SkipLocal);
const auto media = item->media();
const auto updateRecentStickers = inputMedia.has_value()
? Api::HasAttachedStickers(*inputMedia)
: false;
const auto emptyFlag = MTPmessages_EditMessage::Flag(0);
const auto flags = emptyFlag
| ((!text.isEmpty() || media)
? MTPmessages_EditMessage::Flag::f_message
: emptyFlag)
| ((media && inputMedia.has_value())
? MTPmessages_EditMessage::Flag::f_media
: emptyFlag)
| (webpage.removed
? MTPmessages_EditMessage::Flag::f_no_webpage
: emptyFlag)
| ((!webpage.removed && !webpage.url.isEmpty())
? MTPmessages_EditMessage::Flag::f_media
: emptyFlag)
| (((!webpage.removed && !webpage.url.isEmpty() && webpage.invert)
|| options.invertCaption)
? MTPmessages_EditMessage::Flag::f_invert_media
: emptyFlag)
| (!sentEntities.v.isEmpty()
? MTPmessages_EditMessage::Flag::f_entities
: emptyFlag)
| (options.scheduled
? MTPmessages_EditMessage::Flag::f_schedule_date
: emptyFlag)
| ((options.scheduled && options.scheduleRepeatPeriod)
? MTPmessages_EditMessage::Flag::f_schedule_repeat_period
: emptyFlag)
| (item->isBusinessShortcut()
? MTPmessages_EditMessage::Flag::f_quick_reply_shortcut_id
: emptyFlag);
const auto id = item->isScheduled()
? session->scheduledMessages().lookupId(item)
: item->isBusinessShortcut()
? session->data().shortcutMessages().lookupId(item)
: item->id;
return api->request(MTPmessages_EditMessage(
MTP_flags(flags),
item->history()->peer->input(),
MTP_int(id),
MTP_string(text),
inputMedia.value_or(Data::WebPageForMTP(webpage, text.isEmpty())),
MTPReplyMarkup(),
sentEntities,
MTP_int(options.scheduled),
MTP_int(options.scheduleRepeatPeriod),
MTP_int(item->shortcutId())
)).done([=](
const MTPUpdates &result,
[[maybe_unused]] mtpRequestId requestId) {
const auto apply = [=] { api->applyUpdates(result); };
if constexpr (WithId<DoneCallback>) {
done(apply, requestId);
} else if constexpr (WithoutId<DoneCallback>) {
done(apply);
} else if constexpr (WithoutCallback<DoneCallback>) {
done();
apply();
} else {
t_bad_callback(done);
}
if (updateRecentStickers) {
api->requestSpecialStickersForce(false, false, true);
}
}).fail([=](const MTP::Error &error, mtpRequestId requestId) {
if constexpr (ErrorWithId<FailCallback>) {
fail(error.type(), requestId);
} else if constexpr (ErrorWithoutId<FailCallback>) {
fail(error.type());
} else if constexpr (WithoutCallback<FailCallback>) {
fail();
} else {
t_bad_callback(fail);
}
}).send();
}
template <typename DoneCallback, typename FailCallback>
mtpRequestId EditMessage(
not_null<HistoryItem*> item,
SendOptions options,
DoneCallback &&done,
FailCallback &&fail,
std::optional<MTPInputMedia> inputMedia = std::nullopt) {
const auto &text = item->originalText();
const auto webpage = (!item->media() || !item->media()->webpage())
? Data::WebPageDraft{ .removed = true }
: Data::WebPageDraft::FromItem(item);
return EditMessage(
item,
text,
webpage,
options,
std::forward<DoneCallback>(done),
std::forward<FailCallback>(fail),
inputMedia);
}
void EditMessageWithUploadedMedia(
not_null<HistoryItem*> item,
SendOptions options,
MTPInputMedia media) {
const auto done = [=](Fn<void()> applyUpdates) {
if (item) {
item->removeFromSharedMediaIndex();
item->clearSavedMedia();
item->setIsLocalUpdateMedia(true);
applyUpdates();
item->setIsLocalUpdateMedia(false);
}
};
const auto fail = [=](const QString &error) {
const auto session = &item->history()->session();
const auto notModified = (error == u"MESSAGE_NOT_MODIFIED"_q);
const auto mediaInvalid = (error == u"MEDIA_NEW_INVALID"_q);
if (notModified || mediaInvalid) {
item->returnSavedMedia();
session->data().sendHistoryChangeNotifications();
if (mediaInvalid) {
Ui::show(
Ui::MakeInformBox(tr::lng_edit_media_invalid_file()),
Ui::LayerOption::KeepOther);
}
} else {
session->api().sendMessageFail(error, item->history()->peer);
}
};
EditMessage(item, options, done, fail, media);
}
} // namespace
void RescheduleMessage(
not_null<HistoryItem*> item,
SendOptions options) {
const auto empty = [] {};
options.invertCaption = item->invertMedia();
EditMessage(item, options, empty, empty);
}
void EditMessageWithUploadedDocument(
HistoryItem *item,
RemoteFileInfo info,
SendOptions options) {
if (!item || !item->media() || !item->media()->document()) {
return;
}
EditMessageWithUploadedMedia(
item,
options,
PrepareUploadedDocument(item, std::move(info)));
}
void EditMessageWithUploadedPhoto(
HistoryItem *item,
RemoteFileInfo info,
SendOptions options) {
if (!item || !item->media() || !item->media()->photo()) {
return;
}
EditMessageWithUploadedMedia(
item,
options,
PrepareUploadedPhoto(item, std::move(info)));
}
mtpRequestId EditCaption(
not_null<HistoryItem*> item,
const TextWithEntities &caption,
SendOptions options,
Fn<void()> done,
Fn<void(const QString &)> fail) {
return EditMessage(
item,
caption,
Data::WebPageDraft(),
options,
done,
fail);
}
mtpRequestId EditTextMessage(
not_null<HistoryItem*> item,
const TextWithEntities &caption,
Data::WebPageDraft webpage,
SendOptions options,
Fn<void(mtpRequestId requestId)> done,
Fn<void(const QString &error, mtpRequestId requestId)> fail,
bool spoilered) {
const auto media = item->media();
if (media
&& HistoryView::MediaEditManager::CanBeSpoilered(item)
&& spoilered != media->hasSpoiler()) {
auto takeInputMedia = Fn<std::optional<MTPInputMedia>()>(nullptr);
auto takeFileReference = Fn<QByteArray()>(nullptr);
if (const auto photo = media->photo()) {
using Flag = MTPDinputMediaPhoto::Flag;
const auto flags = Flag()
| (media->ttlSeconds() ? Flag::f_ttl_seconds : Flag())
| (spoilered ? Flag::f_spoiler : Flag());
takeInputMedia = [=] {
return MTP_inputMediaPhoto(
MTP_flags(flags),
photo->mtpInput(),
MTP_int(media->ttlSeconds()));
};
takeFileReference = [=] { return photo->fileReference(); };
} else if (const auto document = media->document()) {
using Flag = MTPDinputMediaDocument::Flag;
const auto videoCover = media->videoCover();
const auto videoTimestamp = media->videoTimestamp();
const auto flags = Flag()
| (media->ttlSeconds() ? Flag::f_ttl_seconds : Flag())
| (spoilered ? Flag::f_spoiler : Flag())
| (videoTimestamp ? Flag::f_video_timestamp : Flag())
| (videoCover ? Flag::f_video_cover : Flag());
takeInputMedia = [=] {
return MTP_inputMediaDocument(
MTP_flags(flags),
document->mtpInput(),
(videoCover
? videoCover->mtpInput()
: MTPInputPhoto()),
MTP_int(media->ttlSeconds()),
MTP_int(videoTimestamp),
MTPstring()); // query
};
takeFileReference = [=] { return document->fileReference(); };
}
const auto usedFileReference = takeFileReference
? takeFileReference()
: QByteArray();
const auto origin = item->fullId();
const auto api = &item->history()->session().api();
const auto performRequest = [=](
const auto &repeatRequest,
mtpRequestId originalRequestId) -> mtpRequestId {
const auto handleReference = [=](
const QString &error,
mtpRequestId requestId) {
if (error.startsWith(u"FILE_REFERENCE_"_q)) {
api->refreshFileReference(origin, [=](const auto &) {
if (takeFileReference &&
(takeFileReference() != usedFileReference)) {
repeatRequest(
repeatRequest,
originalRequestId
? originalRequestId
: requestId);
} else {
fail(error, requestId);
}
});
} else {
fail(error, requestId);
}
};
const auto callback = [=](
Fn<void()> applyUpdates,
mtpRequestId requestId) {
applyUpdates();
done(originalRequestId ? originalRequestId : requestId);
};
const auto requestId = EditMessage(
item,
caption,
webpage,
options,
callback,
handleReference,
takeInputMedia ? takeInputMedia() : std::nullopt);
return originalRequestId ? originalRequestId : requestId;
};
return performRequest(performRequest, 0);
}
const auto callback = [=](Fn<void()> applyUpdates, mtpRequestId id) {
applyUpdates();
done(id);
};
return EditMessage(
item,
caption,
webpage,
options,
callback,
fail,
std::nullopt);
}
void EditTodoList(
not_null<HistoryItem*> item,
const TodoListData &data,
SendOptions options,
Fn<void(mtpRequestId requestId)> done,
Fn<void(const QString &error, mtpRequestId requestId)> fail) {
const auto callback = [=](Fn<void()> applyUpdates, mtpRequestId id) {
applyUpdates();
done(id);
};
EditMessage(
item,
options,
callback,
fail,
MTP_inputMediaTodo(TodoListDataToMTP(&data)));
}
} // namespace Api

View File

@@ -0,0 +1,68 @@
/*
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
*/
#pragma once
class HistoryItem;
namespace Data {
struct WebPageDraft;
} // namespace Data
namespace MTP {
class Error;
} // namespace MTP
namespace Api {
struct SendOptions;
struct RemoteFileInfo;
const auto kDefaultEditMessagesErrors = {
u"MESSAGE_ID_INVALID"_q,
u"CHAT_ADMIN_REQUIRED"_q,
u"MESSAGE_EDIT_TIME_EXPIRED"_q,
};
void RescheduleMessage(
not_null<HistoryItem*> item,
SendOptions options);
void EditMessageWithUploadedDocument(
HistoryItem *item,
RemoteFileInfo info,
SendOptions options);
void EditMessageWithUploadedPhoto(
HistoryItem *item,
RemoteFileInfo info,
SendOptions options);
mtpRequestId EditCaption(
not_null<HistoryItem*> item,
const TextWithEntities &caption,
SendOptions options,
Fn<void()> done,
Fn<void(const QString &)> fail);
mtpRequestId EditTextMessage(
not_null<HistoryItem*> item,
const TextWithEntities &caption,
Data::WebPageDraft webpage,
SendOptions options,
Fn<void(mtpRequestId requestId)> done,
Fn<void(const QString &error, mtpRequestId requestId)> fail,
bool spoilered);
void EditTodoList(
not_null<HistoryItem*> item,
const TodoListData &data,
SendOptions options,
Fn<void(mtpRequestId requestId)> done,
Fn<void(const QString &error, mtpRequestId requestId)> fail);
} // namespace Api

View File

@@ -0,0 +1,27 @@
/*
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
*/
#pragma once
namespace Api {
template <typename Type>
void PerformForUpdate(
const MTPUpdates &updates,
Fn<void(const Type &)> callback) {
updates.match([&](const MTPDupdates &updates) {
for (const auto &update : updates.vupdates().v) {
update.match([&](const Type &d) {
callback(d);
}, [](const auto &) {
});
}
}, [](const auto &) {
});
}
} // namespace Api

View File

@@ -0,0 +1,341 @@
/*
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 "api/api_global_privacy.h"
#include "apiwrap.h"
#include "data/components/promo_suggestions.h"
#include "data/data_user.h"
#include "main/main_session.h"
#include "main/main_app_config.h"
namespace Api {
PeerId ParsePaidReactionShownPeer(
not_null<Main::Session*> session,
const MTPPaidReactionPrivacy &value) {
return value.match([&](const MTPDpaidReactionPrivacyDefault &) {
return session->userPeerId();
}, [](const MTPDpaidReactionPrivacyAnonymous &) {
return PeerId();
}, [&](const MTPDpaidReactionPrivacyPeer &data) {
return data.vpeer().match([&](const MTPDinputPeerSelf &) {
return session->userPeerId();
}, [](const MTPDinputPeerUser &data) {
return peerFromUser(data.vuser_id());
}, [](const MTPDinputPeerChat &data) {
return peerFromChat(data.vchat_id());
}, [](const MTPDinputPeerChannel &data) {
return peerFromChannel(data.vchannel_id());
}, [](const MTPDinputPeerUserFromMessage &data) -> PeerId {
Unexpected("From message peer in ParsePaidReactionShownPeer.");
}, [](const MTPDinputPeerChannelFromMessage &data) -> PeerId {
Unexpected("From message peer in ParsePaidReactionShownPeer.");
}, [](const MTPDinputPeerEmpty &) -> PeerId {
Unexpected("Empty peer in ParsePaidReactionShownPeer.");
});
});
}
GlobalPrivacy::GlobalPrivacy(not_null<ApiWrap*> api)
: _session(&api->session())
, _api(&api->instance()) {
}
void GlobalPrivacy::reload(Fn<void()> callback) {
if (callback) {
_callbacks.push_back(std::move(callback));
}
if (_requestId) {
return;
}
_requestId = _api.request(MTPaccount_GetGlobalPrivacySettings(
)).done([=](const MTPGlobalPrivacySettings &result) {
_requestId = 0;
apply(result);
for (const auto &callback : base::take(_callbacks)) {
callback();
}
}).fail([=] {
_requestId = 0;
for (const auto &callback : base::take(_callbacks)) {
callback();
}
}).send();
_session->appConfig().value(
) | rpl::on_next([=] {
_showArchiveAndMute = _session->appConfig().get<bool>(
u"autoarchive_setting_available"_q,
false);
}, _session->lifetime());
}
bool GlobalPrivacy::archiveAndMuteCurrent() const {
return _archiveAndMute.current();
}
rpl::producer<bool> GlobalPrivacy::archiveAndMute() const {
return _archiveAndMute.value();
}
UnarchiveOnNewMessage GlobalPrivacy::unarchiveOnNewMessageCurrent() const {
return _unarchiveOnNewMessage.current();
}
auto GlobalPrivacy::unarchiveOnNewMessage() const
-> rpl::producer<UnarchiveOnNewMessage> {
return _unarchiveOnNewMessage.value();
}
rpl::producer<bool> GlobalPrivacy::showArchiveAndMute() const {
using namespace rpl::mappers;
return rpl::combine(
archiveAndMute(),
_showArchiveAndMute.value(),
_1 || _2);
}
rpl::producer<> GlobalPrivacy::suggestArchiveAndMute() const {
return _session->promoSuggestions().requested(u"AUTOARCHIVE_POPULAR"_q);
}
void GlobalPrivacy::dismissArchiveAndMuteSuggestion() {
_session->promoSuggestions().dismiss(u"AUTOARCHIVE_POPULAR"_q);
}
void GlobalPrivacy::updateHideReadTime(bool hide) {
update(
archiveAndMuteCurrent(),
unarchiveOnNewMessageCurrent(),
hide,
newRequirePremiumCurrent(),
newChargeStarsCurrent(),
disallowedGiftTypesCurrent());
}
bool GlobalPrivacy::hideReadTimeCurrent() const {
return _hideReadTime.current();
}
rpl::producer<bool> GlobalPrivacy::hideReadTime() const {
return _hideReadTime.value();
}
bool GlobalPrivacy::newRequirePremiumCurrent() const {
return _newRequirePremium.current();
}
rpl::producer<bool> GlobalPrivacy::newRequirePremium() const {
return _newRequirePremium.value();
}
int GlobalPrivacy::newChargeStarsCurrent() const {
return _newChargeStars.current();
}
rpl::producer<int> GlobalPrivacy::newChargeStars() const {
return _newChargeStars.value();
}
void GlobalPrivacy::updateMessagesPrivacy(
bool requirePremium,
int chargeStars) {
update(
archiveAndMuteCurrent(),
unarchiveOnNewMessageCurrent(),
hideReadTimeCurrent(),
requirePremium,
chargeStars,
disallowedGiftTypesCurrent());
}
DisallowedGiftTypes GlobalPrivacy::disallowedGiftTypesCurrent() const {
return _disallowedGiftTypes.current();
}
auto GlobalPrivacy::disallowedGiftTypes() const
-> rpl::producer<DisallowedGiftTypes> {
return _disallowedGiftTypes.value();
}
void GlobalPrivacy::updateDisallowedGiftTypes(DisallowedGiftTypes types) {
update(
archiveAndMuteCurrent(),
unarchiveOnNewMessageCurrent(),
hideReadTimeCurrent(),
newRequirePremiumCurrent(),
newChargeStarsCurrent(),
types);
}
void GlobalPrivacy::loadPaidReactionShownPeer() {
if (_paidReactionShownPeerLoaded) {
return;
}
_paidReactionShownPeerLoaded = true;
_api.request(MTPmessages_GetPaidReactionPrivacy(
)).done([=](const MTPUpdates &result) {
_session->api().applyUpdates(result);
}).send();
}
void GlobalPrivacy::updatePaidReactionShownPeer(PeerId shownPeer) {
_paidReactionShownPeer = shownPeer;
}
PeerId GlobalPrivacy::paidReactionShownPeerCurrent() const {
return _paidReactionShownPeer.current();
}
rpl::producer<PeerId> GlobalPrivacy::paidReactionShownPeer() const {
return _paidReactionShownPeer.value();
}
void GlobalPrivacy::updateArchiveAndMute(bool value) {
update(
value,
unarchiveOnNewMessageCurrent(),
hideReadTimeCurrent(),
newRequirePremiumCurrent(),
newChargeStarsCurrent(),
disallowedGiftTypesCurrent());
}
void GlobalPrivacy::updateUnarchiveOnNewMessage(
UnarchiveOnNewMessage value) {
update(
archiveAndMuteCurrent(),
value,
hideReadTimeCurrent(),
newRequirePremiumCurrent(),
newChargeStarsCurrent(),
disallowedGiftTypesCurrent());
}
void GlobalPrivacy::update(
bool archiveAndMute,
UnarchiveOnNewMessage unarchiveOnNewMessage,
bool hideReadTime,
bool newRequirePremium,
int newChargeStars,
DisallowedGiftTypes disallowedGiftTypes) {
using Flag = MTPDglobalPrivacySettings::Flag;
using DisallowedFlag = MTPDdisallowedGiftsSettings::Flag;
_api.request(_requestId).cancel();
const auto newRequirePremiumAllowed = _session->premium()
|| _session->appConfig().newRequirePremiumFree();
const auto showGiftIcon
= (disallowedGiftTypes & DisallowedGiftType::SendHide);
const auto flags = Flag()
| (archiveAndMute
? Flag::f_archive_and_mute_new_noncontact_peers
: Flag())
| (unarchiveOnNewMessage == UnarchiveOnNewMessage::None
? Flag::f_keep_archived_unmuted
: Flag())
| (unarchiveOnNewMessage != UnarchiveOnNewMessage::AnyUnmuted
? Flag::f_keep_archived_folders
: Flag())
| (hideReadTime ? Flag::f_hide_read_marks : Flag())
| ((newRequirePremium && newRequirePremiumAllowed)
? Flag::f_new_noncontact_peers_require_premium
: Flag())
| Flag::f_noncontact_peers_paid_stars
| (showGiftIcon ? Flag::f_display_gifts_button : Flag())
| Flag::f_disallowed_gifts;
const auto disallowedFlags = DisallowedFlag()
| ((disallowedGiftTypes & DisallowedGiftType::Premium)
? DisallowedFlag::f_disallow_premium_gifts
: DisallowedFlag())
| ((disallowedGiftTypes & DisallowedGiftType::Unlimited)
? DisallowedFlag::f_disallow_unlimited_stargifts
: DisallowedFlag())
| ((disallowedGiftTypes & DisallowedGiftType::Limited)
? DisallowedFlag::f_disallow_limited_stargifts
: DisallowedFlag())
| ((disallowedGiftTypes & DisallowedGiftType::Unique)
? DisallowedFlag::f_disallow_unique_stargifts
: DisallowedFlag())
| ((disallowedGiftTypes & DisallowedGiftType::FromChannels)
? DisallowedFlag::f_disallow_stargifts_from_channels
: DisallowedFlag());
const auto typesWas = _disallowedGiftTypes.current();
const auto typesChanged = (typesWas != disallowedGiftTypes);
_requestId = _api.request(MTPaccount_SetGlobalPrivacySettings(
MTP_globalPrivacySettings(
MTP_flags(flags),
MTP_long(newChargeStars),
MTP_disallowedGiftsSettings(MTP_flags(disallowedFlags)))
)).done([=](const MTPGlobalPrivacySettings &result) {
_requestId = 0;
apply(result);
if (typesChanged) {
_session->user()->updateFullForced();
}
}).fail([=](const MTP::Error &error) {
_requestId = 0;
if (error.type() == u"PREMIUM_ACCOUNT_REQUIRED"_q) {
update(
archiveAndMute,
unarchiveOnNewMessage,
hideReadTime,
false,
0,
DisallowedGiftTypes());
}
}).send();
_archiveAndMute = archiveAndMute;
_unarchiveOnNewMessage = unarchiveOnNewMessage;
_hideReadTime = hideReadTime;
_newRequirePremium = newRequirePremium;
_newChargeStars = newChargeStars;
_disallowedGiftTypes = disallowedGiftTypes;
}
void GlobalPrivacy::apply(const MTPGlobalPrivacySettings &settings) {
const auto &data = settings.data();
_archiveAndMute = data.is_archive_and_mute_new_noncontact_peers();
_unarchiveOnNewMessage = data.is_keep_archived_unmuted()
? UnarchiveOnNewMessage::None
: data.is_keep_archived_folders()
? UnarchiveOnNewMessage::NotInFoldersUnmuted
: UnarchiveOnNewMessage::AnyUnmuted;
_hideReadTime = data.is_hide_read_marks();
_newRequirePremium = data.is_new_noncontact_peers_require_premium();
_newChargeStars = data.vnoncontact_peers_paid_stars().value_or_empty();
if (const auto gifts = data.vdisallowed_gifts()) {
const auto &disallow = gifts->data();
_disallowedGiftTypes = DisallowedGiftType()
| (disallow.is_disallow_unlimited_stargifts()
? DisallowedGiftType::Unlimited
: DisallowedGiftType())
| (disallow.is_disallow_limited_stargifts()
? DisallowedGiftType::Limited
: DisallowedGiftType())
| (disallow.is_disallow_unique_stargifts()
? DisallowedGiftType::Unique
: DisallowedGiftType())
| (disallow.is_disallow_premium_gifts()
? DisallowedGiftType::Premium
: DisallowedGiftType())
| (disallow.is_disallow_stargifts_from_channels()
? DisallowedGiftType::FromChannels
: DisallowedGiftType())
| (data.is_display_gifts_button()
? DisallowedGiftType::SendHide
: DisallowedGiftType());
} else {
_disallowedGiftTypes = data.is_display_gifts_button()
? DisallowedGiftType::SendHide
: DisallowedGiftType();
}
}
} // namespace Api

View File

@@ -0,0 +1,111 @@
/*
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
*/
#pragma once
#include "base/flags.h"
#include "mtproto/sender.h"
class ApiWrap;
namespace Main {
class Session;
} // namespace Main
namespace Api {
enum class UnarchiveOnNewMessage {
None,
NotInFoldersUnmuted,
AnyUnmuted,
};
enum class DisallowedGiftType : uchar {
Limited = 0x01,
Unlimited = 0x02,
Unique = 0x04,
FromChannels = 0x08,
Premium = 0x10,
SendHide = 0x20,
};
inline constexpr bool is_flag_type(DisallowedGiftType) { return true; }
using DisallowedGiftTypes = base::flags<DisallowedGiftType>;
[[nodiscard]] PeerId ParsePaidReactionShownPeer(
not_null<Main::Session*> session,
const MTPPaidReactionPrivacy &value);
class GlobalPrivacy final {
public:
explicit GlobalPrivacy(not_null<ApiWrap*> api);
void reload(Fn<void()> callback = nullptr);
void updateArchiveAndMute(bool value);
void updateUnarchiveOnNewMessage(UnarchiveOnNewMessage value);
[[nodiscard]] bool archiveAndMuteCurrent() const;
[[nodiscard]] rpl::producer<bool> archiveAndMute() const;
[[nodiscard]] auto unarchiveOnNewMessageCurrent() const
-> UnarchiveOnNewMessage;
[[nodiscard]] auto unarchiveOnNewMessage() const
-> rpl::producer<UnarchiveOnNewMessage>;
[[nodiscard]] rpl::producer<bool> showArchiveAndMute() const;
[[nodiscard]] rpl::producer<> suggestArchiveAndMute() const;
void dismissArchiveAndMuteSuggestion();
void updateHideReadTime(bool hide);
[[nodiscard]] bool hideReadTimeCurrent() const;
[[nodiscard]] rpl::producer<bool> hideReadTime() const;
[[nodiscard]] bool newRequirePremiumCurrent() const;
[[nodiscard]] rpl::producer<bool> newRequirePremium() const;
[[nodiscard]] int newChargeStarsCurrent() const;
[[nodiscard]] rpl::producer<int> newChargeStars() const;
void updateMessagesPrivacy(bool requirePremium, int chargeStars);
[[nodiscard]] DisallowedGiftTypes disallowedGiftTypesCurrent() const;
[[nodiscard]] auto disallowedGiftTypes() const
-> rpl::producer<DisallowedGiftTypes>;
void updateDisallowedGiftTypes(DisallowedGiftTypes types);
void loadPaidReactionShownPeer();
void updatePaidReactionShownPeer(PeerId shownPeer);
[[nodiscard]] PeerId paidReactionShownPeerCurrent() const;
[[nodiscard]] rpl::producer<PeerId> paidReactionShownPeer() const;
private:
void apply(const MTPGlobalPrivacySettings &settings);
void update(
bool archiveAndMute,
UnarchiveOnNewMessage unarchiveOnNewMessage,
bool hideReadTime,
bool newRequirePremium,
int newChargeStars,
DisallowedGiftTypes disallowedGiftTypes);
const not_null<Main::Session*> _session;
MTP::Sender _api;
mtpRequestId _requestId = 0;
rpl::variable<bool> _archiveAndMute = false;
rpl::variable<UnarchiveOnNewMessage> _unarchiveOnNewMessage
= UnarchiveOnNewMessage::None;
rpl::variable<bool> _showArchiveAndMute = false;
rpl::variable<bool> _hideReadTime = false;
rpl::variable<bool> _newRequirePremium = false;
rpl::variable<int> _newChargeStars = 0;
rpl::variable<DisallowedGiftTypes> _disallowedGiftTypes;
rpl::variable<PeerId> _paidReactionShownPeer = false;
std::vector<Fn<void()>> _callbacks;
bool _paidReactionShownPeerLoaded = false;
};
} // namespace Api

View File

@@ -0,0 +1,139 @@
/*
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 "api/api_hash.h"
#include "data/data_document.h"
#include "data/data_session.h"
#include "data/stickers/data_stickers.h"
#include "main/main_session.h"
namespace Api {
namespace {
[[nodiscard]] uint64 CountDocumentVectorHash(
const QVector<DocumentData*> vector) {
auto result = HashInit();
for (const auto document : vector) {
HashUpdate(result, document->id);
}
return HashFinalize(result);
}
[[nodiscard]] uint64 CountSpecialStickerSetHash(
not_null<Main::Session*> session,
uint64 setId) {
const auto &sets = session->data().stickers().sets();
const auto it = sets.find(setId);
if (it != sets.cend()) {
return CountDocumentVectorHash(it->second->stickers);
}
return 0;
}
[[nodiscard]] uint64 CountStickersOrderHash(
not_null<Main::Session*> session,
const Data::StickersSetsOrder &order,
bool checkOutdatedInfo) {
using Flag = Data::StickersSetFlag;
auto result = HashInit();
bool foundOutdated = false;
const auto &sets = session->data().stickers().sets();
for (auto i = order.cbegin(), e = order.cend(); i != e; ++i) {
auto it = sets.find(*i);
if (it != sets.cend()) {
const auto set = it->second.get();
if (set->id == Data::Stickers::DefaultSetId) {
foundOutdated = true;
} else if (!(set->flags & Flag::Special)
&& !(set->flags & Flag::Archived)) {
HashUpdate(result, set->hash);
}
}
}
return (!checkOutdatedInfo || !foundOutdated)
? HashFinalize(result)
: 0;
}
[[nodiscard]] uint64 CountFeaturedHash(
not_null<Main::Session*> session,
const Data::StickersSetsOrder &order) {
auto result = HashInit();
const auto &sets = session->data().stickers().sets();
for (const auto setId : order) {
HashUpdate(result, setId);
const auto it = sets.find(setId);
if (it != sets.cend()
&& (it->second->flags & Data::StickersSetFlag::Unread)) {
HashUpdate(result, 1);
}
}
return HashFinalize(result);
}
} // namespace
uint64 CountStickersHash(
not_null<Main::Session*> session,
bool checkOutdatedInfo) {
return CountStickersOrderHash(
session,
session->data().stickers().setsOrder(),
checkOutdatedInfo);
}
uint64 CountMasksHash(
not_null<Main::Session*> session,
bool checkOutdatedInfo) {
return CountStickersOrderHash(
session,
session->data().stickers().maskSetsOrder(),
checkOutdatedInfo);
}
uint64 CountCustomEmojiHash(
not_null<Main::Session*> session,
bool checkOutdatedInfo) {
return CountStickersOrderHash(
session,
session->data().stickers().emojiSetsOrder(),
checkOutdatedInfo);
}
uint64 CountRecentStickersHash(
not_null<Main::Session*> session,
bool attached) {
return CountSpecialStickerSetHash(
session,
attached
? Data::Stickers::CloudRecentAttachedSetId
: Data::Stickers::CloudRecentSetId);
}
uint64 CountFavedStickersHash(not_null<Main::Session*> session) {
return CountSpecialStickerSetHash(session, Data::Stickers::FavedSetId);
}
uint64 CountFeaturedStickersHash(not_null<Main::Session*> session) {
return CountFeaturedHash(
session,
session->data().stickers().featuredSetsOrder());
}
uint64 CountFeaturedEmojiHash(not_null<Main::Session*> session) {
return CountFeaturedHash(
session,
session->data().stickers().featuredEmojiSetsOrder());
}
uint64 CountSavedGifsHash(not_null<Main::Session*> session) {
return CountDocumentVectorHash(session->data().stickers().savedGifs());
}
} // namespace Api

View File

@@ -0,0 +1,72 @@
/*
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
*/
#pragma once
namespace Main {
class Session;
} // namespace Main
namespace Api {
[[nodiscard]] uint64 CountStickersHash(
not_null<Main::Session*> session,
bool checkOutdatedInfo = false);
[[nodiscard]] uint64 CountMasksHash(
not_null<Main::Session*> session,
bool checkOutdatedInfo = false);
[[nodiscard]] uint64 CountCustomEmojiHash(
not_null<Main::Session*> session,
bool checkOutdatedInfo = false);
[[nodiscard]] uint64 CountRecentStickersHash(
not_null<Main::Session*> session,
bool attached = false);
[[nodiscard]] uint64 CountFavedStickersHash(
not_null<Main::Session*> session);
[[nodiscard]] uint64 CountFeaturedStickersHash(
not_null<Main::Session*> session);
[[nodiscard]] uint64 CountFeaturedEmojiHash(
not_null<Main::Session*> session);
[[nodiscard]] uint64 CountSavedGifsHash(not_null<Main::Session*> session);
[[nodiscard]] inline uint64 HashInit() {
return 0;
}
inline void HashUpdate(uint64 &already, uint64 value) {
already ^= (already >> 21);
already ^= (already << 35);
already ^= (already >> 4);
already += value;
}
inline void HashUpdate(uint64 &already, int64 value) {
HashUpdate(already, uint64(value));
}
inline void HashUpdate(uint64 &already, uint32 value) {
HashUpdate(already, uint64(value));
}
inline void HashUpdate(uint64 &already, int32 value) {
HashUpdate(already, int64(value));
}
[[nodiscard]] inline uint64 HashFinalize(uint64 already) {
return already;
}
template <typename IntRange>
[[nodiscard]] inline uint64 CountHash(IntRange &&range) {
auto result = HashInit();
for (const auto value : range) {
HashUpdate(result, value);
}
return HashFinalize(result);
}
} // namespace Api

View File

@@ -0,0 +1,807 @@
/*
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 "api/api_invite_links.h"
#include "api/api_chat_participants.h"
#include "data/data_changes.h"
#include "data/data_channel.h"
#include "data/data_chat.h"
#include "data/data_peer.h"
#include "data/data_session.h"
#include "data/data_user.h"
#include "main/main_session.h"
#include "base/unixtime.h"
#include "apiwrap.h"
namespace Api {
namespace {
constexpr auto kFirstPage = 10;
constexpr auto kPerPage = 50;
constexpr auto kJoinedFirstPage = 10;
void BringPermanentToFront(PeerInviteLinks &links) {
auto &list = links.links;
const auto i = ranges::find_if(list, [](const InviteLink &link) {
return link.permanent && !link.revoked;
});
if (i != end(list) && i != begin(list)) {
ranges::rotate(begin(list), i, i + 1);
}
}
void RemovePermanent(PeerInviteLinks &links) {
auto &list = links.links;
list.erase(ranges::remove_if(list, [](const InviteLink &link) {
return link.permanent && !link.revoked;
}), end(list));
}
} // namespace
JoinedByLinkSlice ParseJoinedByLinkSlice(
not_null<PeerData*> peer,
const MTPmessages_ChatInviteImporters &slice) {
auto result = JoinedByLinkSlice();
slice.match([&](const MTPDmessages_chatInviteImporters &data) {
auto &owner = peer->session().data();
owner.processUsers(data.vusers());
result.count = data.vcount().v;
result.users.reserve(data.vimporters().v.size());
for (const auto &importer : data.vimporters().v) {
importer.match([&](const MTPDchatInviteImporter &data) {
result.users.push_back({
.user = owner.user(data.vuser_id()),
.date = data.vdate().v,
.viaFilterLink = data.is_via_chatlist(),
});
});
}
});
return result;
}
InviteLinks::InviteLinks(not_null<ApiWrap*> api) : _api(api) {
}
void InviteLinks::create(const CreateInviteLinkArgs &args) {
performCreate(args, false);
}
void InviteLinks::performCreate(
const CreateInviteLinkArgs &args,
bool revokeLegacyPermanent) {
if (const auto i = _createCallbacks.find(args.peer)
; i != end(_createCallbacks)) {
if (args.done) {
i->second.push_back(std::move(args.done));
}
return;
}
auto &callbacks = _createCallbacks[args.peer];
if (args.done) {
callbacks.push_back(std::move(args.done));
}
const auto requestApproval = !args.subscription && args.requestApproval;
using Flag = MTPmessages_ExportChatInvite::Flag;
_api->request(MTPmessages_ExportChatInvite(
MTP_flags((revokeLegacyPermanent
? Flag::f_legacy_revoke_permanent
: Flag(0))
| (!args.label.isEmpty() ? Flag::f_title : Flag(0))
| (args.expireDate ? Flag::f_expire_date : Flag(0))
| ((!requestApproval && args.usageLimit)
? Flag::f_usage_limit
: Flag(0))
| (requestApproval ? Flag::f_request_needed : Flag(0))
| (args.subscription ? Flag::f_subscription_pricing : Flag(0))),
args.peer->input(),
MTP_int(args.expireDate),
MTP_int(args.usageLimit),
MTP_string(args.label),
MTP_starsSubscriptionPricing(
MTP_int(args.subscription.period),
MTP_long(args.subscription.credits))
)).done([=, peer = args.peer](const MTPExportedChatInvite &result) {
const auto callbacks = _createCallbacks.take(peer);
const auto link = prepend(peer, peer->session().user(), result);
if (link && callbacks) {
for (const auto &callback : *callbacks) {
callback(*link);
}
}
}).fail([=, peer = args.peer] {
_createCallbacks.erase(peer);
}).send();
}
auto InviteLinks::lookupMyPermanent(not_null<PeerData*> peer) -> Link* {
auto i = _firstSlices.find(peer);
return (i != end(_firstSlices)) ? lookupMyPermanent(i->second) : nullptr;
}
auto InviteLinks::lookupMyPermanent(Links &links) -> Link* {
const auto first = links.links.begin();
return (first != end(links.links) && first->permanent && !first->revoked)
? &*first
: nullptr;
}
auto InviteLinks::lookupMyPermanent(const Links &links) const -> const Link* {
const auto first = links.links.begin();
return (first != end(links.links) && first->permanent && !first->revoked)
? &*first
: nullptr;
}
auto InviteLinks::prepend(
not_null<PeerData*> peer,
not_null<UserData*> admin,
const MTPExportedChatInvite &invite) -> std::optional<Link> {
const auto link = parse(peer, invite);
if (!link) {
return link;
}
if (admin->isSelf()) {
prependMyToFirstSlice(peer, admin, *link);
}
_updates.fire(Update{
.peer = peer,
.admin = admin,
.now = *link
});
return link;
}
void InviteLinks::prependMyToFirstSlice(
not_null<PeerData*> peer,
not_null<UserData*> admin,
const Link &link) {
Expects(admin->isSelf());
auto i = _firstSlices.find(peer);
if (i == end(_firstSlices)) {
i = _firstSlices.emplace(peer).first;
}
auto &links = i->second;
const auto permanent = lookupMyPermanent(links);
const auto hadPermanent = (permanent != nullptr);
auto updateOldPermanent = Update{
.peer = peer,
.admin = admin,
};
if (link.permanent && hadPermanent) {
updateOldPermanent.was = permanent->link;
updateOldPermanent.now = *permanent;
updateOldPermanent.now->revoked = true;
links.links.erase(begin(links.links));
if (links.count > 0) {
--links.count;
}
}
// Must not dereference 'permanent' pointer after that.
++links.count;
if (hadPermanent && !link.permanent) {
links.links.insert(begin(links.links) + 1, link);
} else {
links.links.insert(begin(links.links), link);
}
if (link.permanent) {
editPermanentLink(peer, link.link);
}
notify(peer);
if (updateOldPermanent.now) {
_updates.fire(std::move(updateOldPermanent));
}
}
void InviteLinks::edit(
not_null<PeerData*> peer,
not_null<UserData*> admin,
const QString &link,
const QString &label,
TimeId expireDate,
int usageLimit,
bool requestApproval,
Fn<void(Link)> done) {
performEdit(
peer,
admin,
link,
std::move(done),
false,
label,
expireDate,
usageLimit,
requestApproval);
}
void InviteLinks::editTitle(
not_null<PeerData*> peer,
not_null<UserData*> admin,
const QString &link,
const QString &label,
Fn<void(Link)> done) {
performEdit(peer, admin, link, done, false, label, 0, 0, false, true);
}
void InviteLinks::performEdit(
not_null<PeerData*> peer,
not_null<UserData*> admin,
const QString &link,
Fn<void(Link)> done,
bool revoke,
const QString &label,
TimeId expireDate,
int usageLimit,
bool requestApproval,
bool editOnlyTitle) {
const auto key = LinkKey{ peer, link };
if (_deleteCallbacks.contains(key)) {
return;
} else if (const auto i = _editCallbacks.find(key)
; i != end(_editCallbacks)) {
if (done) {
i->second.push_back(std::move(done));
}
return;
}
auto &callbacks = _editCallbacks[key];
if (done) {
callbacks.push_back(std::move(done));
}
using Flag = MTPmessages_EditExportedChatInvite::Flag;
const auto flags = (revoke ? Flag::f_revoked : Flag(0))
| (!revoke ? Flag::f_title : Flag(0))
| (!revoke ? Flag::f_expire_date : Flag(0))
| ((!revoke && !requestApproval) ? Flag::f_usage_limit : Flag(0))
| ((!revoke && (requestApproval || !usageLimit))
? Flag::f_request_needed
: Flag(0));
_api->request(MTPmessages_EditExportedChatInvite(
MTP_flags(editOnlyTitle ? Flag::f_title : flags),
peer->input(),
MTP_string(link),
MTP_int(expireDate),
MTP_int(usageLimit),
MTP_bool(requestApproval),
MTP_string(label)
)).done([=](const MTPmessages_ExportedChatInvite &result) {
const auto callbacks = _editCallbacks.take(key);
const auto peer = key.peer;
result.match([&](const auto &data) {
_api->session().data().processUsers(data.vusers());
const auto link = parse(peer, data.vinvite());
if (!link) {
return;
}
auto i = _firstSlices.find(peer);
if (i != end(_firstSlices)) {
const auto j = ranges::find(
i->second.links,
key.link,
&Link::link);
if (j != end(i->second.links)) {
if (link->revoked && !j->revoked) {
i->second.links.erase(j);
if (i->second.count > 0) {
--i->second.count;
}
} else {
*j = *link;
}
}
}
for (const auto &callback : *callbacks) {
callback(*link);
}
_updates.fire(Update{
.peer = peer,
.admin = admin,
.was = key.link,
.now = link,
});
using Replaced = MTPDmessages_exportedChatInviteReplaced;
if constexpr (Replaced::Is<decltype(data)>()) {
prepend(peer, admin, data.vnew_invite());
}
});
}).fail([=] {
_editCallbacks.erase(key);
}).send();
}
void InviteLinks::revoke(
not_null<PeerData*> peer,
not_null<UserData*> admin,
const QString &link,
Fn<void(Link)> done) {
performEdit(peer, admin, link, std::move(done), true);
}
void InviteLinks::revokePermanent(
not_null<PeerData*> peer,
not_null<UserData*> admin,
const QString &link,
Fn<void()> done) {
const auto callback = [=](auto&&) { done(); };
if (!link.isEmpty()) {
performEdit(peer, admin, link, callback, true);
} else if (!admin->isSelf()) {
crl::on_main(&peer->session(), done);
} else {
performCreate({ peer, callback }, true);
}
}
void InviteLinks::destroy(
not_null<PeerData*> peer,
not_null<UserData*> admin,
const QString &link,
Fn<void()> done) {
const auto key = LinkKey{ peer, link };
if (const auto i = _deleteCallbacks.find(key)
; i != end(_deleteCallbacks)) {
if (done) {
i->second.push_back(std::move(done));
}
return;
}
auto &callbacks = _deleteCallbacks[key];
if (done) {
callbacks.push_back(std::move(done));
}
_api->request(MTPmessages_DeleteExportedChatInvite(
peer->input(),
MTP_string(link)
)).done([=] {
const auto callbacks = _deleteCallbacks.take(key);
if (callbacks) {
for (const auto &callback : *callbacks) {
callback();
}
}
_updates.fire(Update{
.peer = peer,
.admin = admin,
.was = key.link,
});
}).fail([=] {
_deleteCallbacks.erase(key);
}).send();
}
void InviteLinks::destroyAllRevoked(
not_null<PeerData*> peer,
not_null<UserData*> admin,
Fn<void()> done) {
if (const auto i = _deleteRevokedCallbacks.find(peer)
; i != end(_deleteRevokedCallbacks)) {
if (done) {
i->second.push_back(std::move(done));
}
return;
}
auto &callbacks = _deleteRevokedCallbacks[peer];
if (done) {
callbacks.push_back(std::move(done));
}
_api->request(MTPmessages_DeleteRevokedExportedChatInvites(
peer->input(),
admin->inputUser()
)).done([=] {
if (const auto callbacks = _deleteRevokedCallbacks.take(peer)) {
for (const auto &callback : *callbacks) {
callback();
}
}
_allRevokedDestroyed.fire({ peer, admin });
}).send();
}
void InviteLinks::requestMyLinks(not_null<PeerData*> peer) {
if (_firstSliceRequests.contains(peer)) {
return;
}
const auto requestId = _api->request(MTPmessages_GetExportedChatInvites(
MTP_flags(0),
peer->input(),
MTP_inputUserSelf(),
MTPint(), // offset_date
MTPstring(), // offset_link
MTP_int(kFirstPage)
)).done([=](const MTPmessages_ExportedChatInvites &result) {
_firstSliceRequests.remove(peer);
auto slice = parseSlice(peer, result);
auto i = _firstSlices.find(peer);
const auto permanent = (i != end(_firstSlices))
? lookupMyPermanent(i->second)
: nullptr;
if (!permanent) {
BringPermanentToFront(slice);
const auto j = _firstSlices.emplace_or_assign(
peer,
std::move(slice)).first;
if (const auto permanent = lookupMyPermanent(j->second)) {
editPermanentLink(peer, permanent->link);
}
} else {
RemovePermanent(slice);
auto &existing = i->second.links;
existing.erase(begin(existing) + 1, end(existing));
existing.insert(
end(existing),
begin(slice.links),
end(slice.links));
i->second.count = std::max(slice.count, int(existing.size()));
}
notify(peer);
}).fail([=] {
_firstSliceRequests.remove(peer);
}).send();
_firstSliceRequests.emplace(peer, requestId);
}
void InviteLinks::processRequest(
not_null<PeerData*> peer,
const QString &link,
not_null<UserData*> user,
bool approved,
Fn<void()> done,
Fn<void()> fail) {
if (_processRequests.contains({ peer, user })) {
return;
}
_processRequests.emplace(
std::pair{ peer, user },
ProcessRequest{ std::move(done), std::move(fail) });
using Flag = MTPmessages_HideChatJoinRequest::Flag;
_api->request(MTPmessages_HideChatJoinRequest(
MTP_flags(approved ? Flag::f_approved : Flag(0)),
peer->input(),
user->inputUser()
)).done([=](const MTPUpdates &result) {
if (const auto chat = peer->asChat()) {
if (chat->count > 0) {
if (chat->participants.size() >= chat->count) {
chat->participants.emplace(user);
}
++chat->count;
}
} else if (const auto channel = peer->asChannel()) {
_api->chatParticipants().requestCountDelayed(channel);
}
_api->applyUpdates(result);
if (link.isEmpty() && approved) {
// We don't know the link that was used for this user.
// Prune all the cache.
for (auto i = begin(_firstJoined); i != end(_firstJoined);) {
if (i->first.peer == peer) {
i = _firstJoined.erase(i);
} else {
++i;
}
}
_firstSlices.remove(peer);
} else if (approved) {
const auto i = _firstJoined.find({ peer, link });
if (i != end(_firstJoined)) {
++i->second.count;
i->second.users.insert(
begin(i->second.users),
JoinedByLinkUser{ user, base::unixtime::now() });
}
}
if (const auto callbacks = _processRequests.take({ peer, user })) {
if (const auto &done = callbacks->done) {
done();
}
}
}).fail([=] {
if (const auto callbacks = _processRequests.take({ peer, user })) {
if (const auto &fail = callbacks->fail) {
fail();
}
}
}).send();
}
void InviteLinks::applyExternalUpdate(
not_null<PeerData*> peer,
InviteLink updated) {
if (const auto i = _firstSlices.find(peer); i != end(_firstSlices)) {
for (auto &link : i->second.links) {
if (link.link == updated.link) {
link = updated;
}
}
}
_updates.fire({
.peer = peer,
.admin = updated.admin,
.was = updated.link,
.now = updated,
});
}
std::optional<JoinedByLinkSlice> InviteLinks::lookupJoinedFirstSlice(
LinkKey key) const {
const auto i = _firstJoined.find(key);
return (i != end(_firstJoined))
? std::make_optional(i->second)
: std::nullopt;
}
std::optional<JoinedByLinkSlice> InviteLinks::joinedFirstSliceLoaded(
not_null<PeerData*> peer,
const QString &link) const {
return lookupJoinedFirstSlice({ peer, link });
}
rpl::producer<JoinedByLinkSlice> InviteLinks::joinedFirstSliceValue(
not_null<PeerData*> peer,
const QString &link,
int fullCount) {
const auto key = LinkKey{ peer, link };
auto current = lookupJoinedFirstSlice(key).value_or(JoinedByLinkSlice());
if (current.count == fullCount
&& (!fullCount || !current.users.empty())) {
return rpl::single(current);
}
current.count = fullCount;
const auto remove = int(current.users.size()) - current.count;
if (remove > 0) {
current.users.erase(end(current.users) - remove, end(current.users));
}
requestJoinedFirstSlice(key);
using namespace rpl::mappers;
return rpl::single(
current
) | rpl::then(_joinedFirstSliceLoaded.events(
) | rpl::filter(
_1 == key
) | rpl::map([=] {
return lookupJoinedFirstSlice(key).value_or(JoinedByLinkSlice());
}));
}
auto InviteLinks::updates(
not_null<PeerData*> peer,
not_null<UserData*> admin) const -> rpl::producer<Update> {
return _updates.events() | rpl::filter([=](const Update &update) {
return update.peer == peer && update.admin == admin;
});
}
rpl::producer<> InviteLinks::allRevokedDestroyed(
not_null<PeerData*> peer,
not_null<UserData*> admin) const {
return _allRevokedDestroyed.events(
) | rpl::filter([=](const AllRevokedDestroyed &which) {
return which.peer == peer && which.admin == admin;
}) | rpl::to_empty;
}
void InviteLinks::requestJoinedFirstSlice(LinkKey key) {
if (_firstJoinedRequests.contains(key)) {
return;
}
const auto requestId = _api->request(MTPmessages_GetChatInviteImporters(
MTP_flags(MTPmessages_GetChatInviteImporters::Flag::f_link),
key.peer->input(),
MTP_string(key.link),
MTPstring(), // q
MTP_int(0), // offset_date
MTP_inputUserEmpty(), // offset_user
MTP_int(kJoinedFirstPage)
)).done([=](const MTPmessages_ChatInviteImporters &result) {
_firstJoinedRequests.remove(key);
_firstJoined[key] = ParseJoinedByLinkSlice(key.peer, result);
_joinedFirstSliceLoaded.fire_copy(key);
}).fail([=] {
_firstJoinedRequests.remove(key);
}).send();
_firstJoinedRequests.emplace(key, requestId);
}
void InviteLinks::setMyPermanent(
not_null<PeerData*> peer,
const MTPExportedChatInvite &invite) {
auto link = parse(peer, invite);
if (!link) {
LOG(("API Error: "
"InviteLinks::setPermanent called with non-link."));
return;
} else if (!link->permanent) {
LOG(("API Error: "
"InviteLinks::setPermanent called with non-permanent link."));
return;
}
auto i = _firstSlices.find(peer);
if (i == end(_firstSlices)) {
i = _firstSlices.emplace(peer).first;
}
auto &links = i->second;
auto updateOldPermanent = Update{
.peer = peer,
.admin = peer->session().user(),
};
if (const auto permanent = lookupMyPermanent(links)) {
if (permanent->link == link->link) {
if (permanent->usage != link->usage) {
permanent->usage = link->usage;
_updates.fire(Update{
.peer = peer,
.admin = peer->session().user(),
.was = link->link,
.now = *permanent
});
}
return;
}
updateOldPermanent.was = permanent->link;
updateOldPermanent.now = *permanent;
updateOldPermanent.now->revoked = true;
links.links.erase(begin(links.links));
if (links.count > 0) {
--links.count;
}
}
links.links.insert(begin(links.links), *link);
editPermanentLink(peer, link->link);
notify(peer);
if (updateOldPermanent.now) {
_updates.fire(std::move(updateOldPermanent));
}
_updates.fire(Update{
.peer = peer,
.admin = peer->session().user(),
.now = link
});
}
void InviteLinks::clearMyPermanent(not_null<PeerData*> peer) {
auto i = _firstSlices.find(peer);
if (i == end(_firstSlices)) {
return;
}
auto &links = i->second;
const auto permanent = lookupMyPermanent(links);
if (!permanent) {
return;
}
auto updateOldPermanent = Update{
.peer = peer,
.admin = peer->session().user()
};
updateOldPermanent.was = permanent->link;
updateOldPermanent.now = *permanent;
updateOldPermanent.now->revoked = true;
links.links.erase(begin(links.links));
if (links.count > 0) {
--links.count;
}
editPermanentLink(peer, QString());
notify(peer);
if (updateOldPermanent.now) {
_updates.fire(std::move(updateOldPermanent));
}
}
void InviteLinks::notify(not_null<PeerData*> peer) {
peer->session().changes().peerUpdated(
peer,
Data::PeerUpdate::Flag::InviteLinks);
}
auto InviteLinks::myLinks(not_null<PeerData*> peer) const -> const Links & {
static const auto kEmpty = Links();
const auto i = _firstSlices.find(peer);
return (i != end(_firstSlices)) ? i->second : kEmpty;
}
auto InviteLinks::parseSlice(
not_null<PeerData*> peer,
const MTPmessages_ExportedChatInvites &slice) const -> Links {
auto i = _firstSlices.find(peer);
const auto permanent = (i != end(_firstSlices))
? lookupMyPermanent(i->second)
: nullptr;
auto result = Links();
slice.match([&](const MTPDmessages_exportedChatInvites &data) {
peer->session().data().processUsers(data.vusers());
result.count = data.vcount().v;
for (const auto &invite : data.vinvites().v) {
if (const auto link = parse(peer, invite)) {
if (!permanent || link->link != permanent->link) {
result.links.push_back(*link);
}
}
}
});
return result;
}
auto InviteLinks::parse(
not_null<PeerData*> peer,
const MTPExportedChatInvite &invite) const -> std::optional<Link> {
return invite.match([&](const MTPDchatInviteExported &data) {
return std::optional<Link>(Link{
.link = qs(data.vlink()),
.label = qs(data.vtitle().value_or_empty()),
.subscription = data.vsubscription_pricing()
? Data::PeerSubscription{
data.vsubscription_pricing()->data().vamount().v,
data.vsubscription_pricing()->data().vperiod().v,
}
: Data::PeerSubscription(),
.admin = peer->session().data().user(data.vadmin_id()),
.date = data.vdate().v,
.startDate = data.vstart_date().value_or_empty(),
.expireDate = data.vexpire_date().value_or_empty(),
.usageLimit = data.vusage_limit().value_or_empty(),
.usage = data.vusage().value_or_empty(),
.requested = data.vrequested().value_or_empty(),
.requestApproval = data.is_request_needed(),
.permanent = data.is_permanent(),
.revoked = data.is_revoked(),
});
}, [&](const MTPDchatInvitePublicJoinRequests &data) {
return std::optional<Link>();
});
}
void InviteLinks::requestMoreLinks(
not_null<PeerData*> peer,
not_null<UserData*> admin,
TimeId lastDate,
const QString &lastLink,
bool revoked,
Fn<void(Links)> done) {
using Flag = MTPmessages_GetExportedChatInvites::Flag;
_api->request(MTPmessages_GetExportedChatInvites(
MTP_flags(Flag::f_offset_link
| (revoked ? Flag::f_revoked : Flag(0))),
peer->input(),
admin->inputUser(),
MTP_int(lastDate),
MTP_string(lastLink),
MTP_int(kPerPage)
)).done([=](const MTPmessages_ExportedChatInvites &result) {
done(parseSlice(peer, result));
}).fail([=] {
done(Links());
}).send();
}
void InviteLinks::editPermanentLink(
not_null<PeerData*> peer,
const QString &link) {
if (const auto chat = peer->asChat()) {
chat->setInviteLink(link);
} else if (const auto channel = peer->asChannel()) {
channel->setInviteLink(link);
} else {
Unexpected("Peer in InviteLinks::editMainLink.");
}
}
} // namespace Api

View File

@@ -0,0 +1,245 @@
/*
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
*/
#pragma once
class ApiWrap;
#include "data/data_subscriptions.h"
namespace Api {
struct InviteLink {
QString link;
QString label;
Data::PeerSubscription subscription;
not_null<UserData*> admin;
TimeId date = 0;
TimeId startDate = 0;
TimeId expireDate = 0;
int usageLimit = 0;
int usage = 0;
int requested = 0;
bool requestApproval = false;
bool permanent = false;
bool revoked = false;
};
struct PeerInviteLinks {
std::vector<InviteLink> links;
int count = 0;
};
struct JoinedByLinkUser {
not_null<UserData*> user;
TimeId date = 0;
bool viaFilterLink = false;
};
struct JoinedByLinkSlice {
std::vector<JoinedByLinkUser> users;
int count = 0;
};
struct InviteLinkUpdate {
not_null<PeerData*> peer;
not_null<UserData*> admin;
QString was;
std::optional<InviteLink> now;
};
[[nodiscard]] JoinedByLinkSlice ParseJoinedByLinkSlice(
not_null<PeerData*> peer,
const MTPmessages_ChatInviteImporters &slice);
struct CreateInviteLinkArgs {
not_null<PeerData*> peer;
Fn<void(InviteLink)> done = nullptr;
QString label;
TimeId expireDate = 0;
int usageLimit = 0;
bool requestApproval = false;
Data::PeerSubscription subscription;
};
class InviteLinks final {
public:
explicit InviteLinks(not_null<ApiWrap*> api);
using Link = InviteLink;
using Links = PeerInviteLinks;
using Update = InviteLinkUpdate;
void create(const CreateInviteLinkArgs &args);
void edit(
not_null<PeerData*> peer,
not_null<UserData*> admin,
const QString &link,
const QString &label,
TimeId expireDate,
int usageLimit,
bool requestApproval,
Fn<void(Link)> done = nullptr);
void editTitle(
not_null<PeerData*> peer,
not_null<UserData*> admin,
const QString &link,
const QString &label,
Fn<void(Link)> done = nullptr);
void revoke(
not_null<PeerData*> peer,
not_null<UserData*> admin,
const QString &link,
Fn<void(Link)> done = nullptr);
void revokePermanent(
not_null<PeerData*> peer,
not_null<UserData*> admin,
const QString &link,
Fn<void()> done = nullptr);
void destroy(
not_null<PeerData*> peer,
not_null<UserData*> admin,
const QString &link,
Fn<void()> done = nullptr);
void destroyAllRevoked(
not_null<PeerData*> peer,
not_null<UserData*> admin,
Fn<void()> done = nullptr);
void setMyPermanent(
not_null<PeerData*> peer,
const MTPExportedChatInvite &invite);
void clearMyPermanent(not_null<PeerData*> peer);
void requestMyLinks(not_null<PeerData*> peer);
[[nodiscard]] const Links &myLinks(not_null<PeerData*> peer) const;
void processRequest(
not_null<PeerData*> peer,
const QString &link,
not_null<UserData*> user,
bool approved,
Fn<void()> done,
Fn<void()> fail);
void applyExternalUpdate(not_null<PeerData*> peer, InviteLink updated);
[[nodiscard]] rpl::producer<JoinedByLinkSlice> joinedFirstSliceValue(
not_null<PeerData*> peer,
const QString &link,
int fullCount);
[[nodiscard]] std::optional<JoinedByLinkSlice> joinedFirstSliceLoaded(
not_null<PeerData*> peer,
const QString &link) const;
[[nodiscard]] rpl::producer<Update> updates(
not_null<PeerData*> peer,
not_null<UserData*> admin) const;
[[nodiscard]] rpl::producer<> allRevokedDestroyed(
not_null<PeerData*> peer,
not_null<UserData*> admin) const;
void requestMoreLinks(
not_null<PeerData*> peer,
not_null<UserData*> admin,
TimeId lastDate,
const QString &lastLink,
bool revoked,
Fn<void(Links)> done);
private:
struct LinkKey {
not_null<PeerData*> peer;
QString link;
friend inline bool operator<(const LinkKey &a, const LinkKey &b) {
return (a.peer == b.peer)
? (a.link < b.link)
: (a.peer < b.peer);
}
friend inline bool operator==(const LinkKey &a, const LinkKey &b) {
return (a.peer == b.peer) && (a.link == b.link);
}
};
struct ProcessRequest {
Fn<void()> done;
Fn<void()> fail;
};
[[nodiscard]] Links parseSlice(
not_null<PeerData*> peer,
const MTPmessages_ExportedChatInvites &slice) const;
[[nodiscard]] std::optional<Link> parse(
not_null<PeerData*> peer,
const MTPExportedChatInvite &invite) const;
[[nodiscard]] Link *lookupMyPermanent(not_null<PeerData*> peer);
[[nodiscard]] Link *lookupMyPermanent(Links &links);
[[nodiscard]] const Link *lookupMyPermanent(const Links &links) const;
std::optional<Link> prepend(
not_null<PeerData*> peer,
not_null<UserData*> admin,
const MTPExportedChatInvite &invite);
void prependMyToFirstSlice(
not_null<PeerData*> peer,
not_null<UserData*> admin,
const Link &link);
void notify(not_null<PeerData*> peer);
void editPermanentLink(
not_null<PeerData*> peer,
const QString &link);
void performEdit(
not_null<PeerData*> peer,
not_null<UserData*> admin,
const QString &link,
Fn<void(Link)> done,
bool revoke,
const QString &label = QString(),
TimeId expireDate = 0,
int usageLimit = 0,
bool requestApproval = false,
bool editOnlyTitle = false);
void performCreate(
const CreateInviteLinkArgs &args,
bool revokeLegacyPermanent);
void requestJoinedFirstSlice(LinkKey key);
[[nodiscard]] std::optional<JoinedByLinkSlice> lookupJoinedFirstSlice(
LinkKey key) const;
const not_null<ApiWrap*> _api;
base::flat_map<not_null<PeerData*>, Links> _firstSlices;
base::flat_map<not_null<PeerData*>, mtpRequestId> _firstSliceRequests;
base::flat_map<LinkKey, JoinedByLinkSlice> _firstJoined;
base::flat_map<LinkKey, mtpRequestId> _firstJoinedRequests;
rpl::event_stream<LinkKey> _joinedFirstSliceLoaded;
base::flat_map<
not_null<PeerData*>,
std::vector<Fn<void(Link)>>> _createCallbacks;
base::flat_map<LinkKey, std::vector<Fn<void(Link)>>> _editCallbacks;
base::flat_map<LinkKey, std::vector<Fn<void()>>> _deleteCallbacks;
base::flat_map<
not_null<PeerData*>,
std::vector<Fn<void()>>> _deleteRevokedCallbacks;
base::flat_map<
std::pair<not_null<PeerData*>, not_null<UserData*>>,
ProcessRequest> _processRequests;
rpl::event_stream<Update> _updates;
struct AllRevokedDestroyed {
not_null<PeerData*> peer;
not_null<UserData*> admin;
};
rpl::event_stream<AllRevokedDestroyed> _allRevokedDestroyed;
};
} // namespace Api

View File

@@ -0,0 +1,142 @@
/*
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 "api/api_media.h"
#include "api/api_common.h"
#include "data/data_document.h"
#include "data/stickers/data_stickers_set.h"
#include "history/history_item.h"
namespace Api {
namespace {
MTPVector<MTPDocumentAttribute> ComposeSendingDocumentAttributes(
not_null<DocumentData*> document) {
const auto filenameAttribute = MTP_documentAttributeFilename(
MTP_string(document->filename()));
const auto dimensions = document->dimensions;
auto attributes = QVector<MTPDocumentAttribute>(1, filenameAttribute);
if (dimensions.width() > 0 && dimensions.height() > 0) {
if (document->hasDuration() && !document->hasMimeType(u"image/gif"_q)) {
auto flags = MTPDdocumentAttributeVideo::Flags(0);
using VideoFlag = MTPDdocumentAttributeVideo::Flag;
if (document->isVideoMessage()) {
flags |= VideoFlag::f_round_message;
}
if (document->supportsStreaming()) {
flags |= VideoFlag::f_supports_streaming;
}
attributes.push_back(MTP_documentAttributeVideo(
MTP_flags(flags),
MTP_double(document->duration() / 1000.),
MTP_int(dimensions.width()),
MTP_int(dimensions.height()),
MTPint(), // preload_prefix_size
MTPdouble(), // video_start_ts
MTPstring())); // video_codec
} else {
attributes.push_back(MTP_documentAttributeImageSize(
MTP_int(dimensions.width()),
MTP_int(dimensions.height())));
}
}
if (document->type == AnimatedDocument) {
attributes.push_back(MTP_documentAttributeAnimated());
} else if (document->type == StickerDocument && document->sticker()) {
attributes.push_back(MTP_documentAttributeSticker(
MTP_flags(0),
MTP_string(document->sticker()->alt),
Data::InputStickerSet(document->sticker()->set),
MTPMaskCoords()));
} else if (const auto song = document->song()) {
const auto flags = MTPDdocumentAttributeAudio::Flag::f_title
| MTPDdocumentAttributeAudio::Flag::f_performer;
attributes.push_back(MTP_documentAttributeAudio(
MTP_flags(flags),
MTP_int(document->duration() / 1000),
MTP_string(song->title),
MTP_string(song->performer),
MTPstring()));
} else if (const auto voice = document->voice()) {
const auto flags = MTPDdocumentAttributeAudio::Flag::f_voice
| MTPDdocumentAttributeAudio::Flag::f_waveform;
attributes.push_back(MTP_documentAttributeAudio(
MTP_flags(flags),
MTP_int(document->duration() / 1000),
MTPstring(),
MTPstring(),
MTP_bytes(documentWaveformEncode5bit(voice->waveform))));
}
return MTP_vector<MTPDocumentAttribute>(attributes);
}
} // namespace
MTPInputMedia PrepareUploadedPhoto(
not_null<HistoryItem*> item,
RemoteFileInfo info) {
using Flag = MTPDinputMediaUploadedPhoto::Flag;
const auto spoiler = item->media() && item->media()->hasSpoiler();
const auto ttlSeconds = item->media()
? item->media()->ttlSeconds()
: 0;
const auto flags = (spoiler ? Flag::f_spoiler : Flag())
| (info.attachedStickers.empty() ? Flag() : Flag::f_stickers)
| (ttlSeconds ? Flag::f_ttl_seconds : Flag());
return MTP_inputMediaUploadedPhoto(
MTP_flags(flags),
info.file,
MTP_vector<MTPInputDocument>(
ranges::to<QVector<MTPInputDocument>>(info.attachedStickers)),
MTP_int(ttlSeconds));
}
MTPInputMedia PrepareUploadedDocument(
not_null<HistoryItem*> item,
RemoteFileInfo info) {
if (!item || !item->media() || !item->media()->document()) {
return MTP_inputMediaEmpty();
}
using Flag = MTPDinputMediaUploadedDocument::Flag;
const auto spoiler = item->media() && item->media()->hasSpoiler();
const auto ttlSeconds = item->media()
? item->media()->ttlSeconds()
: 0;
const auto flags = (spoiler ? Flag::f_spoiler : Flag())
| (info.thumb ? Flag::f_thumb : Flag())
| (item->groupId() ? Flag::f_nosound_video : Flag())
| (info.attachedStickers.empty() ? Flag::f_stickers : Flag())
| (ttlSeconds ? Flag::f_ttl_seconds : Flag())
| (info.videoCover ? Flag::f_video_cover : Flag());
const auto document = item->media()->document();
return MTP_inputMediaUploadedDocument(
MTP_flags(flags),
info.file,
info.thumb.value_or(MTPInputFile()),
MTP_string(document->mimeString()),
ComposeSendingDocumentAttributes(document),
MTP_vector<MTPInputDocument>(
ranges::to<QVector<MTPInputDocument>>(info.attachedStickers)),
info.videoCover.value_or(MTPInputPhoto()),
MTP_int(0), // video_timestamp
MTP_int(ttlSeconds));
}
bool HasAttachedStickers(MTPInputMedia media) {
return media.match([&](const MTPDinputMediaUploadedPhoto &photo) -> bool {
return (photo.vflags().v
& MTPDinputMediaUploadedPhoto::Flag::f_stickers);
}, [&](const MTPDinputMediaUploadedDocument &document) -> bool {
return (document.vflags().v
& MTPDinputMediaUploadedDocument::Flag::f_stickers);
}, [](const auto &d) {
return false;
});
}
} // namespace Api

View File

@@ -0,0 +1,26 @@
/*
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
*/
#pragma once
class HistoryItem;
namespace Api {
struct RemoteFileInfo;
MTPInputMedia PrepareUploadedPhoto(
not_null<HistoryItem*> item,
RemoteFileInfo info);
MTPInputMedia PrepareUploadedDocument(
not_null<HistoryItem*> item,
RemoteFileInfo info);
bool HasAttachedStickers(MTPInputMedia media);
} // namespace Api

View File

@@ -0,0 +1,212 @@
/*
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 "api/api_messages_search.h"
#include "apiwrap.h"
#include "data/data_channel.h"
#include "data/data_histories.h"
#include "data/data_message_reaction_id.h"
#include "data/data_peer.h"
#include "data/data_session.h"
#include "history/history.h"
#include "history/history_item.h"
#include "main/main_session.h"
namespace Api {
namespace {
constexpr auto kSearchPerPage = 50;
[[nodiscard]] MessageIdsList HistoryItemsFromTL(
not_null<Data::Session*> data,
const QVector<MTPMessage> &messages) {
auto result = MessageIdsList();
for (const auto &message : messages) {
const auto peerId = PeerFromMessage(message);
if (data->peerLoaded(peerId)) {
if (DateFromMessage(message)) {
const auto item = data->addNewMessage(
message,
MessageFlags(),
NewMessageType::Existing);
result.push_back(item->fullId());
}
} else {
LOG(("API Error: a search results with not loaded peer %1"
).arg(peerId.value));
}
}
return result;
}
[[nodiscard]] QString RequestToToken(
const MessagesSearch::Request &request) {
auto result = request.query;
if (request.from) {
result += '\n' + QString::number(request.from->id.value);
}
for (const auto &tag : request.tags) {
result += '\n';
if (const auto customId = tag.custom()) {
result += u"custom"_q + QString::number(customId);
} else {
result += u"emoji"_q + tag.emoji();
}
}
return result;
}
} // namespace
MessagesSearch::MessagesSearch(not_null<History*> history)
: _history(history) {
}
MessagesSearch::~MessagesSearch() {
_history->owner().histories().cancelRequest(
base::take(_searchInHistoryRequest));
}
void MessagesSearch::searchMessages(Request request) {
_request = std::move(request);
_offsetId = {};
searchRequest();
}
void MessagesSearch::searchMore() {
if (_searchInHistoryRequest || _requestId) {
return;
}
searchRequest();
}
void MessagesSearch::searchRequest() {
const auto nextToken = RequestToToken(_request);
if (!_offsetId) {
const auto it = _cacheOfStartByToken.find(nextToken);
if (it != end(_cacheOfStartByToken)) {
_requestId = 0;
searchReceived(it->second, _requestId, nextToken);
return;
}
}
auto callback = [=](Fn<void()> finish) {
using Flag = MTPmessages_Search::Flag;
const auto from = _request.from;
const auto fromPeer = _history->peer->isUser() ? nullptr : from;
const auto savedPeer = _history->peer->isSelf() ? from : nullptr;
_requestId = _history->session().api().request(MTPmessages_Search(
MTP_flags((fromPeer ? Flag::f_from_id : Flag())
| (savedPeer ? Flag::f_saved_peer_id : Flag())
| (_request.topMsgId ? Flag::f_top_msg_id : Flag())
| (_request.tags.empty() ? Flag() : Flag::f_saved_reaction)),
_history->peer->input(),
MTP_string(_request.query),
(fromPeer ? fromPeer->input() : MTP_inputPeerEmpty()),
(savedPeer ? savedPeer->input() : MTP_inputPeerEmpty()),
MTP_vector_from_range(_request.tags | ranges::views::transform(
Data::ReactionToMTP
)),
MTP_int(_request.topMsgId), // top_msg_id
MTP_inputMessagesFilterEmpty(),
MTP_int(0), // min_date
MTP_int(0), // max_date
MTP_int(_offsetId), // offset_id
MTP_int(0), // add_offset
MTP_int(kSearchPerPage),
MTP_int(0), // max_id
MTP_int(0), // min_id
MTP_long(0) // hash
)).done([=](const TLMessages &result, mtpRequestId id) {
_searchInHistoryRequest = 0;
searchReceived(result, id, nextToken);
finish();
}).fail([=](const MTP::Error &error, mtpRequestId id) {
_searchInHistoryRequest = 0;
if (_requestId == id) {
_requestId = 0;
}
if (error.type() == u"SEARCH_QUERY_EMPTY"_q) {
_messagesFounds.fire({ 0, MessageIdsList(), nextToken });
}
finish();
}).send();
return _requestId;
};
_searchInHistoryRequest = _history->owner().histories().sendRequest(
_history,
Data::Histories::RequestType::History,
std::move(callback));
}
void MessagesSearch::searchReceived(
const TLMessages &result,
mtpRequestId requestId,
const QString &nextToken) {
if (requestId != _requestId) {
return;
}
auto &owner = _history->owner();
auto found = result.match([&](const MTPDmessages_messages &data) {
if (_requestId != 0) {
// Don't apply cached data!
owner.processUsers(data.vusers());
owner.processChats(data.vchats());
_history->peer->processTopics(data.vtopics());
}
auto items = HistoryItemsFromTL(&owner, data.vmessages().v);
const auto total = int(data.vmessages().v.size());
return FoundMessages{ total, std::move(items), nextToken };
}, [&](const MTPDmessages_messagesSlice &data) {
if (_requestId != 0) {
// Don't apply cached data!
owner.processUsers(data.vusers());
owner.processChats(data.vchats());
_history->peer->processTopics(data.vtopics());
}
auto items = HistoryItemsFromTL(&owner, data.vmessages().v);
// data.vnext_rate() is used only in global search.
const auto total = int(data.vcount().v);
return FoundMessages{ total, std::move(items), nextToken };
}, [&](const MTPDmessages_channelMessages &data) {
if (_requestId != 0) {
// Don't apply cached data!
owner.processUsers(data.vusers());
owner.processChats(data.vchats());
if (const auto channel = _history->peer->asChannel()) {
channel->ptsReceived(data.vpts().v);
} else {
LOG(("API Error: "
"received messages.channelMessages when no channel "
"was passed!"));
}
_history->peer->processTopics(data.vtopics());
}
auto items = HistoryItemsFromTL(&owner, data.vmessages().v);
const auto total = int(data.vcount().v);
return FoundMessages{ total, std::move(items), nextToken };
}, [](const MTPDmessages_messagesNotModified &data) {
return FoundMessages{};
});
if (!_offsetId) {
_cacheOfStartByToken.emplace(nextToken, result);
}
_requestId = 0;
_offsetId = found.messages.empty()
? MsgId()
: found.messages.back().msg;
_messagesFounds.fire(std::move(found));
}
rpl::producer<FoundMessages> MessagesSearch::messagesFounds() const {
return _messagesFounds.events();
}
} // namespace Api

View File

@@ -0,0 +1,75 @@
/*
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
*/
#pragma once
#include "base/qt/qt_compare.h"
#include "data/data_message_reaction_id.h"
class HistoryItem;
class History;
class PeerData;
namespace Data {
struct ReactionId;
} // namespace Data
namespace Api {
struct FoundMessages {
int total = -1;
MessageIdsList messages;
QString nextToken;
};
class MessagesSearch final {
public:
struct Request {
QString query;
PeerData *from = nullptr;
std::vector<Data::ReactionId> tags;
MsgId topMsgId;
friend inline bool operator==(
const Request &,
const Request &) = default;
friend inline auto operator<=>(
const Request &,
const Request &) = default;
};
explicit MessagesSearch(not_null<History*> history);
~MessagesSearch();
void searchMessages(Request request);
void searchMore();
[[nodiscard]] rpl::producer<FoundMessages> messagesFounds() const;
private:
using TLMessages = MTPmessages_Messages;
void searchRequest();
void searchReceived(
const TLMessages &result,
mtpRequestId requestId,
const QString &nextToken);
const not_null<History*> _history;
base::flat_map<QString, TLMessages> _cacheOfStartByToken;
Request _request;
MsgId _offsetId;
int _searchInHistoryRequest = 0; // Not real mtpRequestId.
mtpRequestId _requestId = 0;
rpl::event_stream<FoundMessages> _messagesFounds;
};
} // namespace Api

View File

@@ -0,0 +1,115 @@
/*
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 "api/api_messages_search_merged.h"
#include "history/history.h"
namespace Api {
MessagesSearchMerged::MessagesSearchMerged(not_null<History*> history)
: _apiSearch(history) {
if (const auto migrated = history->migrateFrom()) {
_migratedSearch.emplace(migrated);
}
const auto checkWaitingForTotal = [=] {
if (_waitingForTotal) {
if (_concatedFound.total >= 0 && _migratedFirstFound.total >= 0) {
_waitingForTotal = false;
_concatedFound.total += _migratedFirstFound.total;
_newFounds.fire({});
}
} else {
_newFounds.fire({});
}
};
const auto checkFull = [=](const FoundMessages &data) {
if (data.total == int(_concatedFound.messages.size())) {
_isFull = true;
addFound(_migratedFirstFound);
}
};
_apiSearch.messagesFounds(
) | rpl::on_next([=](const FoundMessages &data) {
if (data.nextToken == _concatedFound.nextToken) {
addFound(data);
checkFull(data);
_nextFounds.fire({});
} else {
_concatedFound = data;
checkFull(data);
checkWaitingForTotal();
}
}, _lifetime);
if (_migratedSearch) {
_migratedSearch->messagesFounds(
) | rpl::on_next([=](const FoundMessages &data) {
if (_isFull) {
addFound(data);
}
if (data.nextToken == _migratedFirstFound.nextToken) {
_nextFounds.fire({});
} else {
_migratedFirstFound = data;
checkWaitingForTotal();
}
}, _lifetime);
}
}
void MessagesSearchMerged::disableMigrated() {
_migratedSearch = std::nullopt;
}
void MessagesSearchMerged::addFound(const FoundMessages &data) {
for (const auto &message : data.messages) {
_concatedFound.messages.push_back(message);
}
}
const FoundMessages &MessagesSearchMerged::messages() const {
return _concatedFound;
}
const MessagesSearch::Request &MessagesSearchMerged::request() const {
return _request;
}
void MessagesSearchMerged::clear() {
_concatedFound = {};
_migratedFirstFound = {};
}
void MessagesSearchMerged::search(const Request &search) {
_request = search;
if (_migratedSearch) {
_waitingForTotal = true;
_migratedSearch->searchMessages(search);
}
_apiSearch.searchMessages(search);
}
void MessagesSearchMerged::searchMore() {
if (_migratedSearch && _isFull) {
_migratedSearch->searchMore();
} else {
_apiSearch.searchMore();
}
}
rpl::producer<> MessagesSearchMerged::newFounds() const {
return _newFounds.events();
}
rpl::producer<> MessagesSearchMerged::nextFounds() const {
return _nextFounds.events();
}
} // namespace Api

View File

@@ -0,0 +1,61 @@
/*
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
*/
#pragma once
#include "api/api_messages_search.h"
class History;
class PeerData;
namespace Data {
struct ReactionId;
} // namespace Data
namespace Api {
// Search in both of history and migrated history, if it exists.
class MessagesSearchMerged final {
public:
using Request = MessagesSearch::Request;
using CachedRequests = base::flat_set<Request>;
MessagesSearchMerged(not_null<History*> history);
void clear();
void search(const Request &search);
void searchMore();
void disableMigrated();
[[nodiscard]] const FoundMessages &messages() const;
[[nodiscard]] const Request &request() const;
[[nodiscard]] rpl::producer<> newFounds() const;
[[nodiscard]] rpl::producer<> nextFounds() const;
private:
void addFound(const FoundMessages &data);
MessagesSearch _apiSearch;
Request _request;
std::optional<MessagesSearch> _migratedSearch;
FoundMessages _migratedFirstFound;
FoundMessages _concatedFound;
bool _waitingForTotal = false;
bool _isFull = false;
rpl::event_stream<> _newFounds;
rpl::event_stream<> _nextFounds;
rpl::lifetime _lifetime;
};
} // namespace Api

View File

@@ -0,0 +1,278 @@
/*
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 "api/api_peer_colors.h"
#include "apiwrap.h"
#include "data/data_peer.h"
#include "window/themes/window_theme.h"
#include "ui/chat/chat_style.h"
#include "ui/color_int_conversion.h"
namespace Api {
namespace {
constexpr auto kRequestEach = 3600 * crl::time(1000);
} // namespace
PeerColors::PeerColors(not_null<ApiWrap*> api)
: _api(&api->instance())
, _timer([=] { request(); requestProfile(); }) {
request();
requestProfile();
_timer.callEach(kRequestEach);
}
PeerColors::~PeerColors() = default;
void PeerColors::request() {
if (_requestId) {
return;
}
_requestId = _api.request(MTPhelp_GetPeerColors(
MTP_int(_hash)
)).done([=](const MTPhelp_PeerColors &result) {
_requestId = 0;
result.match([&](const MTPDhelp_peerColors &data) {
_hash = data.vhash().v;
apply(data);
}, [](const MTPDhelp_peerColorsNotModified &) {
});
}).fail([=] {
_requestId = 0;
}).send();
}
void PeerColors::requestProfile() {
if (_profileRequestId) {
return;
}
_profileRequestId = _api.request(MTPhelp_GetPeerProfileColors(
MTP_int(_profileHash)
)).done([=](const MTPhelp_PeerColors &result) {
_profileRequestId = 0;
result.match([&](const MTPDhelp_peerColors &data) {
_profileHash = data.vhash().v;
applyProfile(data);
}, [](const MTPDhelp_peerColorsNotModified &) {
});
}).fail([=] {
_profileRequestId = 0;
}).send();
}
std::vector<uint8> PeerColors::suggested() const {
return _suggested.current();
}
rpl::producer<std::vector<uint8>> PeerColors::suggestedValue() const {
return _suggested.value();
}
auto PeerColors::indicesValue() const
-> rpl::producer<Ui::ColorIndicesCompressed> {
return rpl::single(
indicesCurrent()
) | rpl::then(_colorIndicesChanged.events() | rpl::map([=] {
return indicesCurrent();
}));
}
Ui::ColorIndicesCompressed PeerColors::indicesCurrent() const {
return _colorIndicesCurrent
? *_colorIndicesCurrent
: Ui::ColorIndicesCompressed();
}
const base::flat_map<uint8, int> &PeerColors::requiredLevelsGroup() const {
return _requiredLevelsGroup;
}
const base::flat_map<uint8, int> &PeerColors::requiredLevelsChannel() const {
return _requiredLevelsChannel;
}
int PeerColors::requiredLevelFor(
PeerId channel,
uint8 index,
bool isMegagroup,
bool profile) const {
if (Data::DecideColorIndex(channel) == index) {
return 0;
}
if (profile) {
const auto it = _profileColors.find(index);
if (it != end(_profileColors)) {
return isMegagroup
? it->second.requiredLevelsGroup
: it->second.requiredLevelsChannel;
}
return 1;
}
const auto &levels = isMegagroup
? _requiredLevelsGroup
: _requiredLevelsChannel;
if (const auto i = levels.find(index); i != end(levels)) {
return i->second;
}
return 1;
}
void PeerColors::apply(const MTPDhelp_peerColors &data) {
auto suggested = std::vector<uint8>();
auto colors = std::make_shared<
std::array<Ui::ColorIndexData, Ui::kColorIndexCount>>();
using ParsedColor = std::array<uint32, Ui::kColorPatternsCount>;
const auto parseColors = [](const MTPhelp_PeerColorSet &set) {
return set.match([&](const MTPDhelp_peerColorSet &data) {
auto result = ParsedColor();
const auto &list = data.vcolors().v;
if (list.empty() || list.size() > Ui::kColorPatternsCount) {
LOG(("API Error: Bad count for PeerColorSet.colors: %1"
).arg(list.size()));
return ParsedColor();
}
auto fill = result.data();
for (const auto &color : list) {
*fill++ = (uint32(1) << 24) | uint32(color.v);
}
return result;
}, [](const MTPDhelp_peerColorProfileSet &) {
LOG(("API Error: peerColorProfileSet in colors result!"));
return ParsedColor();
});
};
const auto &list = data.vcolors().v;
_requiredLevelsGroup.clear();
_requiredLevelsChannel.clear();
suggested.reserve(list.size());
for (const auto &color : list) {
const auto &data = color.data();
const auto colorIndexBare = data.vcolor_id().v;
if (colorIndexBare < 0 || colorIndexBare >= Ui::kColorIndexCount) {
LOG(("API Error: Bad color index: %1").arg(colorIndexBare));
continue;
}
const auto colorIndex = uint8(colorIndexBare);
if (const auto min = data.vgroup_min_level()) {
_requiredLevelsGroup[colorIndex] = min->v;
}
if (const auto min = data.vchannel_min_level()) {
_requiredLevelsChannel[colorIndex] = min->v;
}
if (!data.is_hidden()) {
suggested.push_back(colorIndex);
}
if (const auto light = data.vcolors()) {
auto &fields = (*colors)[colorIndex];
fields.light = parseColors(*light);
if (const auto dark = data.vdark_colors()) {
fields.dark = parseColors(*dark);
} else {
fields.dark = fields.light;
}
}
}
if (!_colorIndicesCurrent) {
_colorIndicesCurrent = std::make_unique<Ui::ColorIndicesCompressed>(
Ui::ColorIndicesCompressed{ std::move(colors) });
_colorIndicesChanged.fire({});
} else if (*_colorIndicesCurrent->colors != *colors) {
_colorIndicesCurrent->colors = std::move(colors);
_colorIndicesChanged.fire({});
}
_suggested = std::move(suggested);
}
void PeerColors::applyProfile(const MTPDhelp_peerColors &data) {
const auto parseColors = [](const MTPhelp_PeerColorSet &set) {
const auto toUint = [](const MTPint &c) {
return (uint32(1) << 24) | uint32(c.v);
};
return set.match([&](const MTPDhelp_peerColorSet &) {
LOG(("API Error: peerColorSet in profile colors result!"));
return Data::ColorProfileSet();
}, [&](const MTPDhelp_peerColorProfileSet &data) {
auto set = Data::ColorProfileSet();
set.palette.reserve(data.vpalette_colors().v.size());
set.bg.reserve(data.vbg_colors().v.size());
set.story.reserve(data.vstory_colors().v.size());
for (const auto &c : data.vpalette_colors().v) {
set.palette.push_back(Ui::ColorFromSerialized(toUint(c)));
}
for (const auto &c : data.vbg_colors().v) {
set.bg.push_back(Ui::ColorFromSerialized(toUint(c)));
}
for (const auto &c : data.vstory_colors().v) {
set.story.push_back(Ui::ColorFromSerialized(toUint(c)));
}
return set;
});
};
auto suggested = std::vector<Data::ColorProfileData>();
const auto &list = data.vcolors().v;
suggested.reserve(list.size());
for (const auto &color : list) {
const auto &data = color.data();
const auto colorIndexBare = data.vcolor_id().v;
if (colorIndexBare < 0 || colorIndexBare >= Ui::kColorIndexCount) {
LOG(("API Error: Bad color index: %1").arg(colorIndexBare));
continue;
}
const auto colorIndex = uint8(colorIndexBare);
auto result = ProfileColorOption();
result.isHidden = data.is_hidden();
if (const auto min = data.vgroup_min_level()) {
result.requiredLevelsGroup = min->v;
}
if (const auto min = data.vchannel_min_level()) {
result.requiredLevelsChannel = min->v;
}
if (const auto light = data.vcolors()) {
result.data.light = parseColors(*light);
}
if (const auto dark = data.vdark_colors()) {
result.data.dark = parseColors(*dark);
}
_profileColors[colorIndex] = std::move(result);
}
}
std::optional<Data::ColorProfileSet> PeerColors::colorProfileFor(
not_null<PeerData*> peer) const {
if (const auto colorProfileIndex = peer->colorProfileIndex()) {
return colorProfileFor(*colorProfileIndex);
}
return std::nullopt;
}
std::optional<Data::ColorProfileSet> PeerColors::colorProfileFor(
uint8 index) const {
const auto i = _profileColors.find(index);
if (i != end(_profileColors)) {
return Window::Theme::IsNightMode()
? i->second.data.dark
: i->second.data.light;
}
return std::nullopt;
}
std::vector<uint8> PeerColors::profileColorIndices() const {
auto result = std::vector<uint8>();
result.reserve(_profileColors.size());
for (const auto &[index, option] : _profileColors) {
result.push_back(index);
}
return result;
}
} // namespace Api

View File

@@ -0,0 +1,80 @@
/*
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
*/
#pragma once
#include "base/timer.h"
#include "data/data_peer_colors.h"
#include "mtproto/sender.h"
class ApiWrap;
namespace Ui {
struct ColorIndicesCompressed;
} // namespace Ui
namespace Api {
class PeerColors final {
public:
explicit PeerColors(not_null<ApiWrap*> api);
~PeerColors();
[[nodiscard]] std::vector<uint8> suggested() const;
[[nodiscard]] rpl::producer<std::vector<uint8>> suggestedValue() const;
[[nodiscard]] Ui::ColorIndicesCompressed indicesCurrent() const;
[[nodiscard]] auto indicesValue() const
-> rpl::producer<Ui::ColorIndicesCompressed>;
[[nodiscard]] auto requiredLevelsGroup() const
-> const base::flat_map<uint8, int> &;
[[nodiscard]] auto requiredLevelsChannel() const
-> const base::flat_map<uint8, int> &;
[[nodiscard]] int requiredLevelFor(
PeerId channel,
uint8 index,
bool isMegagroup,
bool profile) const;
[[nodiscard]] std::optional<Data::ColorProfileSet> colorProfileFor(
not_null<PeerData*> peer) const;
[[nodiscard]] std::optional<Data::ColorProfileSet> colorProfileFor(
uint8 index) const;
[[nodiscard]] std::vector<uint8> profileColorIndices() const;
private:
struct ProfileColorOption {
Data::ColorProfileData data;
int requiredLevelsChannel = 0;
int requiredLevelsGroup = 0;
bool isHidden = false;
};
void request();
void requestProfile();
void apply(const MTPDhelp_peerColors &data);
void applyProfile(const MTPDhelp_peerColors &data);
MTP::Sender _api;
int32 _hash = 0;
int32 _profileHash = 0;
mtpRequestId _requestId = 0;
mtpRequestId _profileRequestId = 0;
base::Timer _timer;
rpl::variable<std::vector<uint8>> _suggested;
base::flat_map<uint8, int> _requiredLevelsGroup;
base::flat_map<uint8, int> _requiredLevelsChannel;
rpl::event_stream<> _colorIndicesChanged;
std::unique_ptr<Ui::ColorIndicesCompressed> _colorIndicesCurrent;
base::flat_map<uint8, ProfileColorOption> _profileColors;
};
} // namespace Api

View File

@@ -0,0 +1,592 @@
/*
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 "api/api_peer_photo.h"
#include "api/api_updates.h"
#include "apiwrap.h"
#include "base/random.h"
#include "base/unixtime.h"
#include "data/stickers/data_stickers.h"
#include "data/data_channel.h"
#include "data/data_chat.h"
#include "data/data_document.h"
#include "data/data_file_origin.h"
#include "data/data_peer.h"
#include "data/data_photo.h"
#include "data/data_session.h"
#include "data/data_user.h"
#include "data/data_user_photos.h"
#include "history/history.h"
#include "main/main_session.h"
#include "storage/file_upload.h"
#include "storage/localimageloader.h"
#include "storage/storage_user_photos.h"
#include <QtCore/QBuffer>
namespace Api {
namespace {
constexpr auto kSharedMediaLimit = 100;
[[nodiscard]] std::shared_ptr<FilePrepareResult> PreparePeerPhoto(
MTP::DcId dcId,
PeerId peerId,
QImage &&image) {
PreparedPhotoThumbs photoThumbs;
QVector<MTPPhotoSize> photoSizes;
QByteArray jpeg;
QBuffer jpegBuffer(&jpeg);
image.save(&jpegBuffer, "JPG", 87);
const auto scaled = [&](int size) {
return image.scaled(
size,
size,
Qt::KeepAspectRatio,
Qt::SmoothTransformation);
};
const auto push = [&](
const char *type,
QImage &&image,
QByteArray bytes = QByteArray()) {
photoSizes.push_back(MTP_photoSize(
MTP_string(type),
MTP_int(image.width()),
MTP_int(image.height()), MTP_int(0)));
photoThumbs.emplace(type[0], PreparedPhotoThumb{
.image = std::move(image),
.bytes = std::move(bytes)
});
};
push("a", scaled(160));
push("b", scaled(320));
push("c", std::move(image), jpeg);
const auto id = base::RandomValue<PhotoId>();
const auto photo = MTP_photo(
MTP_flags(0),
MTP_long(id),
MTP_long(0),
MTP_bytes(),
MTP_int(base::unixtime::now()),
MTP_vector<MTPPhotoSize>(photoSizes),
MTPVector<MTPVideoSize>(),
MTP_int(dcId));
auto result = MakePreparedFile({
.id = id,
.type = SendMediaType::Photo,
});
result->type = SendMediaType::Photo;
result->setFileData(jpeg);
result->thumbId = id;
result->thumbname = "thumb.jpg";
result->photo = photo;
result->photoThumbs = photoThumbs;
return result;
}
[[nodiscard]] std::optional<MTPVideoSize> PrepareMtpMarkup(
not_null<Main::Session*> session,
const PeerPhoto::UserPhoto &d) {
const auto &documentId = d.markupDocumentId;
const auto &colors = d.markupColors;
if (!documentId || colors.empty()) {
return std::nullopt;
}
const auto document = session->data().document(documentId);
if (const auto sticker = document->sticker()) {
if (sticker->isStatic()) {
return std::nullopt;
}
const auto serializeColor = [](const QColor &color) {
return (quint32(std::clamp(color.red(), 0, 255)) << 16)
| (quint32(std::clamp(color.green(), 0, 255)) << 8)
| quint32(std::clamp(color.blue(), 0, 255));
};
auto mtpColors = QVector<MTPint>();
mtpColors.reserve(colors.size());
ranges::transform(
colors,
ranges::back_inserter(mtpColors),
[&](const QColor &c) { return MTP_int(serializeColor(c)); });
if (sticker->setType == Data::StickersType::Emoji) {
return MTP_videoSizeEmojiMarkup(
MTP_long(document->id),
MTP_vector(mtpColors));
} else if (sticker->set.id && sticker->set.accessHash) {
return MTP_videoSizeStickerMarkup(
MTP_inputStickerSetID(
MTP_long(sticker->set.id),
MTP_long(sticker->set.accessHash)),
MTP_long(document->id),
MTP_vector(mtpColors));
} else if (!sticker->set.shortName.isEmpty()) {
return MTP_videoSizeStickerMarkup(
MTP_inputStickerSetShortName(
MTP_string(sticker->set.shortName)),
MTP_long(document->id),
MTP_vector(mtpColors));
} else {
return MTP_videoSizeEmojiMarkup(
MTP_long(document->id),
MTP_vector(mtpColors));
}
}
return std::nullopt;
}
} // namespace
PeerPhoto::PeerPhoto(not_null<ApiWrap*> api)
: _session(&api->session())
, _api(&api->instance()) {
crl::on_main(_session, [=] {
// You can't use _session->lifetime() in the constructor,
// only queued, because it is not constructed yet.
_session->uploader().photoReady(
) | rpl::on_next([=](const Storage::UploadedMedia &data) {
ready(data.fullId, data.info.file, std::nullopt);
}, _session->lifetime());
});
}
void PeerPhoto::upload(
not_null<PeerData*> peer,
UserPhoto &&photo,
Fn<void()> done) {
upload(peer, std::move(photo), UploadType::Default, std::move(done));
}
void PeerPhoto::uploadFallback(not_null<PeerData*> peer, UserPhoto &&photo) {
upload(peer, std::move(photo), UploadType::Fallback, nullptr);
}
void PeerPhoto::updateSelf(
not_null<PhotoData*> photo,
Data::FileOrigin origin,
Fn<void()> done) {
const auto send = [=](auto resend) -> void {
const auto usedFileReference = photo->fileReference();
_api.request(MTPphotos_UpdateProfilePhoto(
MTP_flags(0),
MTPInputUser(), // bot
photo->mtpInput()
)).done([=](const MTPphotos_Photo &result) {
result.match([&](const MTPDphotos_photo &data) {
_session->data().processPhoto(data.vphoto());
_session->data().processUsers(data.vusers());
});
if (done) {
done();
}
}).fail([=](const MTP::Error &error) {
if (error.code() == 400
&& error.type().startsWith(u"FILE_REFERENCE_"_q)) {
photo->session().api().refreshFileReference(origin, [=](
const auto &) {
if (photo->fileReference() != usedFileReference) {
resend(resend);
}
});
}
}).send();
};
send(send);
}
void PeerPhoto::upload(
not_null<PeerData*> peer,
UserPhoto &&photo,
UploadType type,
Fn<void()> done) {
peer = peer->migrateToOrMe();
const auto mtpMarkup = PrepareMtpMarkup(_session, photo);
const auto fakeId = FullMsgId(
peer->id,
_session->data().nextLocalMessageId());
const auto already = ranges::find(
_uploads,
peer,
[](const auto &pair) { return pair.second.peer; });
if (already != end(_uploads)) {
_session->uploader().cancel(already->first);
_uploads.erase(already);
}
_uploads.emplace(
fakeId,
UploadValue{ peer, type, std::move(done) });
if (mtpMarkup) {
ready(fakeId, std::nullopt, mtpMarkup);
} else {
const auto ready = PreparePeerPhoto(
_api.instance().mainDcId(),
peer->id,
base::take(photo.image));
_session->uploader().upload(fakeId, ready);
}
}
void PeerPhoto::suggest(not_null<PeerData*> peer, UserPhoto &&photo) {
upload(peer, std::move(photo), UploadType::Suggestion, nullptr);
}
void PeerPhoto::clear(not_null<PhotoData*> photo) {
const auto self = _session->user();
if (self->userpicPhotoId() == photo->id) {
const auto photoId = photo->id;
const auto peerId = self->id;
_api.request(MTPphotos_UpdateProfilePhoto(
MTP_flags(0),
MTPInputUser(), // bot
MTP_inputPhotoEmpty()
)).done([=](const MTPphotos_Photo &result) {
self->setPhoto(MTP_userProfilePhotoEmpty());
_session->storage().remove(
Storage::UserPhotosRemoveOne(peerToUser(peerId), photoId));
}).send();
} else if (photo->peer && photo->peer->userpicPhotoId() == photo->id) {
const auto applier = [=](const MTPUpdates &result) {
_session->updates().applyUpdates(result);
};
if (const auto chat = photo->peer->asChat()) {
_api.request(MTPmessages_EditChatPhoto(
chat->inputChat(),
MTP_inputChatPhotoEmpty()
)).done(applier).send();
} else if (const auto channel = photo->peer->asChannel()) {
_api.request(MTPchannels_EditPhoto(
channel->inputChannel(),
MTP_inputChatPhotoEmpty()
)).done(applier).send();
}
} else {
const auto fallbackPhotoId = SyncUserFallbackPhotoViewer(self);
if (fallbackPhotoId && (*fallbackPhotoId) == photo->id) {
_api.request(MTPphotos_UpdateProfilePhoto(
MTP_flags(MTPphotos_UpdateProfilePhoto::Flag::f_fallback),
MTPInputUser(), // bot
MTP_inputPhotoEmpty()
)).send();
_session->storage().add(Storage::UserPhotosSetBack(
peerToUser(self->id),
PhotoId()));
} else {
_api.request(MTPphotos_DeletePhotos(
MTP_vector<MTPInputPhoto>(1, photo->mtpInput())
)).send();
_session->storage().remove(Storage::UserPhotosRemoveOne(
peerToUser(self->id),
photo->id));
}
}
}
void PeerPhoto::clearPersonal(not_null<UserData*> user) {
_api.request(MTPphotos_UploadContactProfilePhoto(
MTP_flags(MTPphotos_UploadContactProfilePhoto::Flag::f_save),
user->inputUser(),
MTPInputFile(),
MTPInputFile(), // video
MTPdouble(), // video_start_ts
MTPVideoSize() // video_emoji_markup
)).done([=](const MTPphotos_Photo &result) {
result.match([&](const MTPDphotos_photo &data) {
_session->data().processPhoto(data.vphoto());
_session->data().processUsers(data.vusers());
});
}).send();
if (!user->userpicPhotoUnknown() && user->hasPersonalPhoto()) {
_session->storage().remove(Storage::UserPhotosRemoveOne(
peerToUser(user->id),
user->userpicPhotoId()));
}
}
void PeerPhoto::set(not_null<PeerData*> peer, not_null<PhotoData*> photo) {
if (peer->userpicPhotoId() == photo->id) {
return;
}
if (peer == _session->user()) {
const auto photoId = photo->id;
const auto peerId = peer->id;
_api.request(MTPphotos_UpdateProfilePhoto(
MTP_flags(0),
MTPInputUser(), // bot
photo->mtpInput()
)).done([=](const MTPphotos_Photo &result) {
const auto newPhoto = _session->data().processPhoto(
result.data().vphoto());
_session->data().processUsers(result.data().vusers());
_session->storage().replace(Storage::UserPhotosReplace(
peerToUser(peerId),
photoId,
newPhoto->id));
}).send();
} else {
const auto applier = [=](const MTPUpdates &result) {
_session->updates().applyUpdates(result);
};
if (const auto chat = peer->asChat()) {
_api.request(MTPmessages_EditChatPhoto(
chat->inputChat(),
MTP_inputChatPhoto(photo->mtpInput())
)).done(applier).send();
} else if (const auto channel = peer->asChannel()) {
_api.request(MTPchannels_EditPhoto(
channel->inputChannel(),
MTP_inputChatPhoto(photo->mtpInput())
)).done(applier).send();
}
}
}
void PeerPhoto::ready(
const FullMsgId &msgId,
std::optional<MTPInputFile> file,
std::optional<MTPVideoSize> videoSize) {
const auto maybeUploadValue = _uploads.take(msgId);
if (!maybeUploadValue) {
return;
}
const auto peer = maybeUploadValue->peer;
const auto type = maybeUploadValue->type;
const auto done = maybeUploadValue->done;
const auto applier = [=](const MTPUpdates &result) {
_session->updates().applyUpdates(result);
if (done) {
done();
}
};
const auto botUserInput = [&] {
const auto user = peer->asUser();
return (user && user->botInfo && user->botInfo->canEditInformation)
? std::make_optional<MTPInputUser>(user->inputUser())
: std::nullopt;
}();
if (peer->isSelf() || botUserInput) {
using Flag = MTPphotos_UploadProfilePhoto::Flag;
const auto none = MTPphotos_UploadProfilePhoto::Flags(0);
_api.request(MTPphotos_UploadProfilePhoto(
MTP_flags((file ? Flag::f_file : none)
| (botUserInput ? Flag::f_bot : none)
| (videoSize ? Flag::f_video_emoji_markup : none)
| ((type == UploadType::Fallback) ? Flag::f_fallback : none)),
botUserInput ? (*botUserInput) : MTPInputUser(), // bot
file ? (*file) : MTPInputFile(),
MTPInputFile(), // video
MTPdouble(), // video_start_ts
videoSize ? (*videoSize) : MTPVideoSize() // video_emoji_markup
)).done([=](const MTPphotos_Photo &result) {
const auto photoId = _session->data().processPhoto(
result.data().vphoto())->id;
_session->data().processUsers(result.data().vusers());
if (type == UploadType::Fallback) {
_session->storage().add(Storage::UserPhotosSetBack(
peerToUser(peer->id),
photoId));
} else {
_session->storage().add(Storage::UserPhotosAddNew(
peerToUser(peer->id),
photoId));
}
if (done) {
done();
}
}).send();
} else if (const auto chat = peer->asChat()) {
const auto history = _session->data().history(chat);
using Flag = MTPDinputChatUploadedPhoto::Flag;
const auto none = MTPDinputChatUploadedPhoto::Flags(0);
history->sendRequestId = _api.request(MTPmessages_EditChatPhoto(
chat->inputChat(),
MTP_inputChatUploadedPhoto(
MTP_flags((file ? Flag::f_file : none)
| (videoSize ? Flag::f_video_emoji_markup : none)),
file ? (*file) : MTPInputFile(),
MTPInputFile(), // video
MTPdouble(), // video_start_ts
videoSize ? (*videoSize) : MTPVideoSize()) // video_emoji_markup
)).done(applier).afterRequest(history->sendRequestId).send();
} else if (const auto channel = peer->asChannel()) {
using Flag = MTPDinputChatUploadedPhoto::Flag;
const auto none = MTPDinputChatUploadedPhoto::Flags(0);
const auto history = _session->data().history(channel);
history->sendRequestId = _api.request(MTPchannels_EditPhoto(
channel->inputChannel(),
MTP_inputChatUploadedPhoto(
MTP_flags((file ? Flag::f_file : none)
| (videoSize ? Flag::f_video_emoji_markup : none)),
file ? (*file) : MTPInputFile(),
MTPInputFile(), // video
MTPdouble(), // video_start_ts
videoSize ? (*videoSize) : MTPVideoSize()) // video_emoji_markup
)).done(applier).afterRequest(history->sendRequestId).send();
} else if (const auto user = peer->asUser()) {
using Flag = MTPphotos_UploadContactProfilePhoto::Flag;
const auto none = MTPphotos_UploadContactProfilePhoto::Flags(0);
_api.request(MTPphotos_UploadContactProfilePhoto(
MTP_flags((file ? Flag::f_file : none)
| (videoSize ? Flag::f_video_emoji_markup : none)
| ((type == UploadType::Suggestion)
? Flag::f_suggest
: Flag::f_save)),
user->inputUser(),
file ? (*file) : MTPInputFile(),
MTPInputFile(), // video
MTPdouble(), // video_start_ts
videoSize ? (*videoSize) : MTPVideoSize() // video_emoji_markup
)).done([=](const MTPphotos_Photo &result) {
result.match([&](const MTPDphotos_photo &data) {
_session->data().processPhoto(data.vphoto());
_session->data().processUsers(data.vusers());
});
if (type != UploadType::Suggestion) {
user->updateFullForced();
}
if (done) {
done();
}
}).send();
}
}
void PeerPhoto::requestUserPhotos(
not_null<UserData*> user,
UserPhotoId afterId) {
if (_userPhotosRequests.contains(user)) {
return;
}
const auto requestId = _api.request(MTPphotos_GetUserPhotos(
user->inputUser(),
MTP_int(0),
MTP_long(afterId),
MTP_int(kSharedMediaLimit)
)).done([this, user](const MTPphotos_Photos &result) {
_userPhotosRequests.remove(user);
auto fullCount = result.match([](const MTPDphotos_photos &d) {
return int(d.vphotos().v.size());
}, [](const MTPDphotos_photosSlice &d) {
return d.vcount().v;
});
auto &owner = _session->data();
auto photoIds = result.match([&](const auto &data) {
owner.processUsers(data.vusers());
auto photoIds = std::vector<PhotoId>();
photoIds.reserve(data.vphotos().v.size());
for (const auto &single : data.vphotos().v) {
const auto photo = owner.processPhoto(single);
if (!photo->isNull()) {
photoIds.push_back(photo->id);
}
}
return photoIds;
});
if (!user->userpicPhotoUnknown() && user->hasPersonalPhoto()) {
const auto photo = owner.photo(user->userpicPhotoId());
if (!photo->isNull()) {
++fullCount;
photoIds.insert(begin(photoIds), photo->id);
}
}
_session->storage().add(Storage::UserPhotosAddSlice(
peerToUser(user->id),
std::move(photoIds),
fullCount
));
}).fail([this, user] {
_userPhotosRequests.remove(user);
}).send();
_userPhotosRequests.emplace(user, requestId);
}
auto PeerPhoto::emojiList(EmojiListType type) -> EmojiListData & {
switch (type) {
case EmojiListType::Profile: return _profileEmojiList;
case EmojiListType::Group: return _groupEmojiList;
case EmojiListType::Background: return _backgroundEmojiList;
case EmojiListType::NoChannelStatus: return _noChannelStatusEmojiList;
}
Unexpected("Type in PeerPhoto::emojiList.");
}
auto PeerPhoto::emojiList(EmojiListType type) const
-> const EmojiListData & {
return const_cast<PeerPhoto*>(this)->emojiList(type);
}
void PeerPhoto::requestEmojiList(EmojiListType type) {
auto &list = emojiList(type);
if (list.requestId) {
return;
}
const auto send = [&](auto &&request) {
return _api.request(
std::move(request)
).done([=](const MTPEmojiList &result) {
auto &list = emojiList(type);
list.requestId = 0;
result.match([](const MTPDemojiListNotModified &data) {
}, [&](const MTPDemojiList &data) {
list.list = ranges::views::all(
data.vdocument_id().v
) | ranges::views::transform(
&MTPlong::v
) | ranges::to_vector;
});
}).fail([=] {
emojiList(type).requestId = 0;
}).send();
};
list.requestId = (type == EmojiListType::Profile)
? send(MTPaccount_GetDefaultProfilePhotoEmojis())
: (type == EmojiListType::Group)
? send(MTPaccount_GetDefaultGroupPhotoEmojis())
: (type == EmojiListType::NoChannelStatus)
? send(MTPaccount_GetChannelRestrictedStatusEmojis())
: send(MTPaccount_GetDefaultBackgroundEmojis());
}
rpl::producer<PeerPhoto::EmojiList> PeerPhoto::emojiListValue(
EmojiListType type) {
auto &list = emojiList(type);
if (list.list.current().empty() && !list.requestId) {
requestEmojiList(type);
}
return list.list.value();
}
// Non-personal photo in case a personal photo is set.
void PeerPhoto::registerNonPersonalPhoto(
not_null<UserData*> user,
not_null<PhotoData*> photo) {
_nonPersonalPhotos.emplace_or_assign(user, photo);
}
void PeerPhoto::unregisterNonPersonalPhoto(not_null<UserData*> user) {
_nonPersonalPhotos.erase(user);
}
PhotoData *PeerPhoto::nonPersonalPhoto(
not_null<UserData*> user) const {
const auto i = _nonPersonalPhotos.find(user);
return (i != end(_nonPersonalPhotos)) ? i->second.get() : nullptr;
}
} // namespace Api

View File

@@ -0,0 +1,120 @@
/*
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
*/
#pragma once
#include "mtproto/sender.h"
class ApiWrap;
class PeerData;
class UserData;
namespace Data {
struct FileOrigin;
} // namespace Data
namespace Main {
class Session;
} // namespace Main
namespace Api {
class PeerPhoto final {
public:
using UserPhotoId = PhotoId;
explicit PeerPhoto(not_null<ApiWrap*> api);
enum class EmojiListType {
Profile,
Group,
Background,
NoChannelStatus,
};
struct UserPhoto {
QImage image;
DocumentId markupDocumentId = 0;
std::vector<QColor> markupColors;
};
void upload(
not_null<PeerData*> peer,
UserPhoto &&photo,
Fn<void()> done = nullptr);
void uploadFallback(not_null<PeerData*> peer, UserPhoto &&photo);
void updateSelf(
not_null<PhotoData*> photo,
Data::FileOrigin origin,
Fn<void()> done = nullptr);
void suggest(not_null<PeerData*> peer, UserPhoto &&photo);
void clear(not_null<PhotoData*> photo);
void clearPersonal(not_null<UserData*> user);
void set(not_null<PeerData*> peer, not_null<PhotoData*> photo);
void requestUserPhotos(not_null<UserData*> user, UserPhotoId afterId);
void requestEmojiList(EmojiListType type);
using EmojiList = std::vector<DocumentId>;
[[nodiscard]] rpl::producer<EmojiList> emojiListValue(EmojiListType type);
// Non-personal photo in case a personal photo is set.
void registerNonPersonalPhoto(
not_null<UserData*> user,
not_null<PhotoData*> photo);
void unregisterNonPersonalPhoto(not_null<UserData*> user);
[[nodiscard]] PhotoData *nonPersonalPhoto(
not_null<UserData*> user) const;
private:
enum class UploadType {
Default,
Suggestion,
Fallback,
};
struct EmojiListData {
rpl::variable<EmojiList> list;
mtpRequestId requestId = 0;
};
void ready(
const FullMsgId &msgId,
std::optional<MTPInputFile> file,
std::optional<MTPVideoSize> videoSize);
void upload(
not_null<PeerData*> peer,
UserPhoto &&photo,
UploadType type,
Fn<void()> done);
[[nodiscard]] EmojiListData &emojiList(EmojiListType type);
[[nodiscard]] const EmojiListData &emojiList(EmojiListType type) const;
const not_null<Main::Session*> _session;
MTP::Sender _api;
struct UploadValue {
not_null<PeerData*> peer;
UploadType type = UploadType::Default;
Fn<void()> done;
};
base::flat_map<FullMsgId, UploadValue> _uploads;
base::flat_map<not_null<UserData*>, mtpRequestId> _userPhotosRequests;
base::flat_map<
not_null<UserData*>,
not_null<PhotoData*>> _nonPersonalPhotos;
EmojiListData _profileEmojiList;
EmojiListData _groupEmojiList;
EmojiListData _backgroundEmojiList;
EmojiListData _noChannelStatusEmojiList;
};
} // namespace Api

View File

@@ -0,0 +1,170 @@
/*
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 "api/api_peer_search.h"
#include "api/api_single_message_search.h"
#include "apiwrap.h"
#include "data/data_session.h"
#include "dialogs/ui/chat_search_in.h" // IsHashOrCashtagSearchQuery
#include "main/main_session.h"
namespace Api {
namespace {
constexpr auto kMinSponsoredQueryLength = 4;
} // namespace
PeerSearch::PeerSearch(not_null<Main::Session*> session, Type type)
: _session(session)
, _type(type) {
}
PeerSearch::~PeerSearch() {
clear();
}
void PeerSearch::request(
const QString &query,
Fn<void(PeerSearchResult)> callback,
RequestType type) {
using namespace Dialogs;
_query = Api::ConvertPeerSearchQuery(query);
_callback = callback;
if (_query.isEmpty()
|| IsHashOrCashtagSearchQuery(_query) != HashOrCashtag::None) {
finish(PeerSearchResult{});
return;
}
auto &cache = _cache[_query];
if (cache.peersReady && cache.sponsoredReady) {
finish(cache.result);
return;
} else if (type == RequestType::CacheOnly) {
_callback = nullptr;
return;
} else if (cache.requested) {
return;
}
cache.requested = true;
cache.result.query = _query;
if (_query.size() < kMinSponsoredQueryLength) {
cache.sponsoredReady = true;
} else if (_type == Type::WithSponsored) {
requestSponsored();
}
requestPeers();
}
void PeerSearch::requestPeers() {
const auto requestId = _session->api().request(MTPcontacts_Search(
MTP_string(_query),
MTP_int(SearchPeopleLimit)
)).done([=](const MTPcontacts_Found &result, mtpRequestId requestId) {
const auto &data = result.data();
_session->data().processUsers(data.vusers());
_session->data().processChats(data.vchats());
auto parsed = PeerSearchResult();
parsed.my.reserve(data.vmy_results().v.size());
for (const auto &id : data.vmy_results().v) {
const auto peerId = peerFromMTP(id);
parsed.my.push_back(_session->data().peer(peerId));
}
parsed.peers.reserve(data.vresults().v.size());
for (const auto &id : data.vresults().v) {
const auto peerId = peerFromMTP(id);
parsed.peers.push_back(_session->data().peer(peerId));
}
finishPeers(requestId, std::move(parsed));
}).fail([=](const MTP::Error &error, mtpRequestId requestId) {
finishPeers(requestId, PeerSearchResult{});
}).send();
_peerRequests.emplace(requestId, _query);
}
void PeerSearch::requestSponsored() {
const auto requestId = _session->api().request(
MTPcontacts_GetSponsoredPeers(MTP_string(_query))
).done([=](
const MTPcontacts_SponsoredPeers &result,
mtpRequestId requestId) {
result.match([&](const MTPDcontacts_sponsoredPeersEmpty &) {
finishSponsored(requestId, PeerSearchResult{});
}, [&](const MTPDcontacts_sponsoredPeers &data) {
_session->data().processUsers(data.vusers());
_session->data().processChats(data.vchats());
auto parsed = PeerSearchResult();
parsed.sponsored.reserve(data.vpeers().v.size());
for (const auto &peer : data.vpeers().v) {
const auto &data = peer.data();
const auto peerId = peerFromMTP(data.vpeer());
parsed.sponsored.push_back({
.peer = _session->data().peer(peerId),
.randomId = data.vrandom_id().v,
.sponsorInfo = TextWithEntities::Simple(
qs(data.vsponsor_info().value_or_empty())),
.additionalInfo = TextWithEntities::Simple(
qs(data.vadditional_info().value_or_empty())),
});
}
finishSponsored(requestId, std::move(parsed));
});
}).fail([=](const MTP::Error &error, mtpRequestId requestId) {
finishSponsored(requestId, PeerSearchResult{});
}).send();
_sponsoredRequests.emplace(requestId, _query);
}
void PeerSearch::finishPeers(
mtpRequestId requestId,
PeerSearchResult result) {
const auto query = _peerRequests.take(requestId);
Assert(query.has_value());
auto &cache = _cache[*query];
cache.peersReady = true;
cache.result.my = std::move(result.my);
cache.result.peers = std::move(result.peers);
if (cache.sponsoredReady && _query == *query) {
finish(cache.result);
}
}
void PeerSearch::finishSponsored(
mtpRequestId requestId,
PeerSearchResult result) {
const auto query = _sponsoredRequests.take(requestId);
Assert(query.has_value());
auto &cache = _cache[*query];
cache.sponsoredReady = true;
cache.result.sponsored = std::move(result.sponsored);
if (cache.peersReady && _query == *query) {
finish(cache.result);
}
}
void PeerSearch::finish(PeerSearchResult result) {
if (const auto onstack = base::take(_callback)) {
onstack(std::move(result));
}
}
void PeerSearch::clear() {
_query = QString();
_callback = nullptr;
_cache.clear();
for (const auto &[requestId, query] : base::take(_peerRequests)) {
_session->api().request(requestId).cancel();
}
for (const auto &[requestId, query] : base::take(_sponsoredRequests)) {
_session->api().request(requestId).cancel();
}
}
} // namespace Api

View File

@@ -0,0 +1,76 @@
/*
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
*/
#pragma once
namespace Main {
class Session;
} // namespace Main
namespace Api {
struct SponsoredSearchResult {
not_null<PeerData*> peer;
QByteArray randomId;
TextWithEntities sponsorInfo;
TextWithEntities additionalInfo;
};
struct PeerSearchResult {
QString query;
std::vector<not_null<PeerData*>> my;
std::vector<not_null<PeerData*>> peers;
std::vector<SponsoredSearchResult> sponsored;
};
class PeerSearch final {
public:
enum class Type {
WithSponsored,
JustPeers,
};
PeerSearch(not_null<Main::Session*> session, Type type);
~PeerSearch();
enum class RequestType {
CacheOnly,
CacheOrRemote,
};
void request(
const QString &query,
Fn<void(PeerSearchResult)> callback,
RequestType type = RequestType::CacheOrRemote);
void clear();
private:
struct CacheEntry {
PeerSearchResult result;
bool requested = false;
bool peersReady = false;
bool sponsoredReady = false;
};
void requestPeers();
void requestSponsored();
void finish(PeerSearchResult result);
void finishPeers(mtpRequestId requestId, PeerSearchResult result);
void finishSponsored(mtpRequestId requestId, PeerSearchResult result);
const not_null<Main::Session*> _session;
const Type _type;
QString _query;
Fn<void(PeerSearchResult)> _callback;
base::flat_map<QString, CacheEntry> _cache;
base::flat_map<mtpRequestId, QString> _peerRequests;
base::flat_map<mtpRequestId, QString> _sponsoredRequests;
};
} // namespace Api

View File

@@ -0,0 +1,226 @@
/*
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 "api/api_polls.h"
#include "api/api_common.h"
#include "api/api_updates.h"
#include "apiwrap.h"
#include "base/random.h"
#include "data/business/data_shortcut_messages.h"
#include "data/data_changes.h"
#include "data/data_histories.h"
#include "data/data_poll.h"
#include "data/data_session.h"
#include "history/history.h"
#include "history/history_item.h"
#include "history/history_item_helpers.h" // ShouldSendSilent
#include "main/main_session.h"
namespace Api {
Polls::Polls(not_null<ApiWrap*> api)
: _session(&api->session())
, _api(&api->instance()) {
}
void Polls::create(
const PollData &data,
SendAction action,
Fn<void()> done,
Fn<void()> fail) {
_session->api().sendAction(action);
const auto history = action.history;
const auto peer = history->peer;
const auto topicRootId = action.replyTo.messageId
? action.replyTo.topicRootId
: 0;
const auto monoforumPeerId = action.replyTo.monoforumPeerId;
auto sendFlags = MTPmessages_SendMedia::Flags(0);
if (action.replyTo) {
sendFlags |= MTPmessages_SendMedia::Flag::f_reply_to;
}
const auto clearCloudDraft = action.clearDraft;
if (clearCloudDraft) {
sendFlags |= MTPmessages_SendMedia::Flag::f_clear_draft;
history->clearLocalDraft(topicRootId, monoforumPeerId);
history->clearCloudDraft(topicRootId, monoforumPeerId);
history->startSavingCloudDraft(topicRootId, monoforumPeerId);
}
const auto silentPost = ShouldSendSilent(peer, action.options);
const auto starsPaid = std::min(
peer->starsPerMessageChecked(),
action.options.starsApproved);
if (silentPost) {
sendFlags |= MTPmessages_SendMedia::Flag::f_silent;
}
if (action.options.scheduled) {
sendFlags |= MTPmessages_SendMedia::Flag::f_schedule_date;
if (action.options.scheduleRepeatPeriod) {
sendFlags |= MTPmessages_SendMedia::Flag::f_schedule_repeat_period;
}
}
if (action.options.shortcutId) {
sendFlags |= MTPmessages_SendMedia::Flag::f_quick_reply_shortcut;
}
if (action.options.effectId) {
sendFlags |= MTPmessages_SendMedia::Flag::f_effect;
}
if (action.options.suggest) {
sendFlags |= MTPmessages_SendMedia::Flag::f_suggested_post;
}
if (starsPaid) {
action.options.starsApproved -= starsPaid;
sendFlags |= MTPmessages_SendMedia::Flag::f_allow_paid_stars;
}
const auto sendAs = action.options.sendAs;
if (sendAs) {
sendFlags |= MTPmessages_SendMedia::Flag::f_send_as;
}
auto &histories = history->owner().histories();
const auto randomId = base::RandomValue<uint64>();
histories.sendPreparedMessage(
history,
action.replyTo,
randomId,
Data::Histories::PrepareMessage<MTPmessages_SendMedia>(
MTP_flags(sendFlags),
peer->input(),
Data::Histories::ReplyToPlaceholder(),
PollDataToInputMedia(&data),
MTP_string(),
MTP_long(randomId),
MTPReplyMarkup(),
MTPVector<MTPMessageEntity>(),
MTP_int(action.options.scheduled),
MTP_int(action.options.scheduleRepeatPeriod),
(sendAs ? sendAs->input() : MTP_inputPeerEmpty()),
Data::ShortcutIdToMTP(_session, action.options.shortcutId),
MTP_long(action.options.effectId),
MTP_long(starsPaid),
SuggestToMTP(action.options.suggest)
), [=](const MTPUpdates &result, const MTP::Response &response) {
if (clearCloudDraft) {
history->finishSavingCloudDraft(
topicRootId,
monoforumPeerId,
UnixtimeFromMsgId(response.outerMsgId));
}
_session->changes().historyUpdated(
history,
(action.options.scheduled
? Data::HistoryUpdate::Flag::ScheduledSent
: Data::HistoryUpdate::Flag::MessageSent));
done();
}, [=](const MTP::Error &error, const MTP::Response &response) {
if (clearCloudDraft) {
history->finishSavingCloudDraft(
topicRootId,
monoforumPeerId,
UnixtimeFromMsgId(response.outerMsgId));
}
fail();
});
}
void Polls::sendVotes(
FullMsgId itemId,
const std::vector<QByteArray> &options) {
if (_pollVotesRequestIds.contains(itemId)) {
return;
}
const auto item = _session->data().message(itemId);
const auto media = item ? item->media() : nullptr;
const auto poll = media ? media->poll() : nullptr;
if (!item) {
return;
}
const auto showSending = poll && !options.empty();
const auto hideSending = [=] {
if (showSending) {
if (const auto item = _session->data().message(itemId)) {
poll->sendingVotes.clear();
_session->data().requestItemRepaint(item);
}
}
};
if (showSending) {
poll->sendingVotes = options;
_session->data().requestItemRepaint(item);
}
auto prepared = QVector<MTPbytes>();
prepared.reserve(options.size());
ranges::transform(
options,
ranges::back_inserter(prepared),
[](const QByteArray &option) { return MTP_bytes(option); });
const auto requestId = _api.request(MTPmessages_SendVote(
item->history()->peer->input(),
MTP_int(item->id),
MTP_vector<MTPbytes>(prepared)
)).done([=](const MTPUpdates &result) {
_pollVotesRequestIds.erase(itemId);
hideSending();
_session->updates().applyUpdates(result);
}).fail([=] {
_pollVotesRequestIds.erase(itemId);
hideSending();
}).send();
_pollVotesRequestIds.emplace(itemId, requestId);
}
void Polls::close(not_null<HistoryItem*> item) {
const auto itemId = item->fullId();
if (_pollCloseRequestIds.contains(itemId)) {
return;
}
const auto media = item ? item->media() : nullptr;
const auto poll = media ? media->poll() : nullptr;
if (!poll) {
return;
}
const auto requestId = _api.request(MTPmessages_EditMessage(
MTP_flags(MTPmessages_EditMessage::Flag::f_media),
item->history()->peer->input(),
MTP_int(item->id),
MTPstring(),
PollDataToInputMedia(poll, true),
MTPReplyMarkup(),
MTPVector<MTPMessageEntity>(),
MTP_int(0), // schedule_date
MTP_int(0), // schedule_repeat_period
MTPint() // quick_reply_shortcut_id
)).done([=](const MTPUpdates &result) {
_pollCloseRequestIds.erase(itemId);
_session->updates().applyUpdates(result);
}).fail([=] {
_pollCloseRequestIds.erase(itemId);
}).send();
_pollCloseRequestIds.emplace(itemId, requestId);
}
void Polls::reloadResults(not_null<HistoryItem*> item) {
const auto itemId = item->fullId();
if (!item->isRegular() || _pollReloadRequestIds.contains(itemId)) {
return;
}
const auto requestId = _api.request(MTPmessages_GetPollResults(
item->history()->peer->input(),
MTP_int(item->id)
)).done([=](const MTPUpdates &result) {
_pollReloadRequestIds.erase(itemId);
_session->updates().applyUpdates(result);
}).fail([=] {
_pollReloadRequestIds.erase(itemId);
}).send();
_pollReloadRequestIds.emplace(itemId, requestId);
}
} // namespace Api

View File

@@ -0,0 +1,49 @@
/*
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
*/
#pragma once
#include "mtproto/sender.h"
class ApiWrap;
class HistoryItem;
struct PollData;
namespace Main {
class Session;
} // namespace Main
namespace Api {
struct SendAction;
class Polls final {
public:
explicit Polls(not_null<ApiWrap*> api);
void create(
const PollData &data,
SendAction action,
Fn<void()> done,
Fn<void()> fail);
void sendVotes(
FullMsgId itemId,
const std::vector<QByteArray> &options);
void close(not_null<HistoryItem*> item);
void reloadResults(not_null<HistoryItem*> item);
private:
const not_null<Main::Session*> _session;
MTP::Sender _api;
base::flat_map<FullMsgId, mtpRequestId> _pollVotesRequestIds;
base::flat_map<FullMsgId, mtpRequestId> _pollCloseRequestIds;
base::flat_map<FullMsgId, mtpRequestId> _pollReloadRequestIds;
};
} // namespace Api

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,289 @@
/*
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
*/
#pragma once
#include "data/data_premium_subscription_option.h"
#include "data/data_star_gift.h"
#include "mtproto/sender.h"
class History;
class ApiWrap;
namespace Main {
class Session;
} // namespace Main
namespace Payments {
struct InvoicePremiumGiftCode;
} // namespace Payments
namespace Api {
struct GiftCode {
PeerId from = 0;
PeerId to = 0;
MsgId giveawayId = 0;
TimeId date = 0;
TimeId used = 0; // 0 if not used.
int days = 0;
bool giveaway = false;
explicit operator bool() const {
return days != 0;
}
friend inline bool operator==(
const GiftCode&,
const GiftCode&) = default;
};
enum class GiveawayState {
Invalid,
Running,
Preparing,
Finished,
Refunded,
};
struct GiveawayInfo {
QString giftCode;
QString disallowedCountry;
ChannelId adminChannelId = 0;
GiveawayState state = GiveawayState::Invalid;
TimeId tooEarlyDate = 0;
TimeId finishDate = 0;
TimeId startDate = 0;
uint64 credits = 0;
int winnersCount = 0;
int activatedCount = 0;
bool participating = false;
explicit operator bool() const {
return state != GiveawayState::Invalid;
}
};
struct GiftOptionData {
int64 cost = 0;
QString currency;
int months = 0;
};
class Premium final {
public:
explicit Premium(not_null<ApiWrap*> api);
void reload();
[[nodiscard]] rpl::producer<TextWithEntities> statusTextValue() const;
[[nodiscard]] auto videos() const
-> const base::flat_map<QString, not_null<DocumentData*>> &;
[[nodiscard]] rpl::producer<> videosUpdated() const;
[[nodiscard]] auto stickers() const
-> const std::vector<not_null<DocumentData*>> &;
[[nodiscard]] rpl::producer<> stickersUpdated() const;
[[nodiscard]] auto cloudSet() const
-> const std::vector<not_null<DocumentData*>> &;
[[nodiscard]] rpl::producer<> cloudSetUpdated() const;
[[nodiscard]] auto helloStickers() const
-> const std::vector<not_null<DocumentData*>> &;
[[nodiscard]] rpl::producer<> helloStickersUpdated() const;
[[nodiscard]] int64 monthlyAmount() const;
[[nodiscard]] QString monthlyCurrency() const;
void checkGiftCode(
const QString &slug,
Fn<void(GiftCode)> done);
GiftCode updateGiftCode(const QString &slug, const GiftCode &code);
[[nodiscard]] rpl::producer<GiftCode> giftCodeValue(
const QString &slug) const;
void applyGiftCode(const QString &slug, Fn<void(QString)> done);
void resolveGiveawayInfo(
not_null<PeerData*> peer,
MsgId messageId,
Fn<void(GiveawayInfo)> done);
[[nodiscard]] auto subscriptionOptions() const
-> const Data::PremiumSubscriptionOptions &;
[[nodiscard]] auto someMessageMoneyRestrictionsResolved() const
-> rpl::producer<>;
void resolveMessageMoneyRestrictions(not_null<UserData*> user);
private:
void reloadPromo();
void reloadStickers();
void reloadCloudSet();
void reloadHelloStickers();
void requestPremiumRequiredSlice();
const not_null<Main::Session*> _session;
MTP::Sender _api;
mtpRequestId _promoRequestId = 0;
std::optional<TextWithEntities> _statusText;
rpl::event_stream<TextWithEntities> _statusTextUpdates;
base::flat_map<QString, not_null<DocumentData*>> _videos;
rpl::event_stream<> _videosUpdated;
mtpRequestId _stickersRequestId = 0;
uint64 _stickersHash = 0;
std::vector<not_null<DocumentData*>> _stickers;
rpl::event_stream<> _stickersUpdated;
mtpRequestId _cloudSetRequestId = 0;
uint64 _cloudSetHash = 0;
std::vector<not_null<DocumentData*>> _cloudSet;
rpl::event_stream<> _cloudSetUpdated;
mtpRequestId _helloStickersRequestId = 0;
uint64 _helloStickersHash = 0;
std::vector<not_null<DocumentData*>> _helloStickers;
rpl::event_stream<> _helloStickersUpdated;
int64 _monthlyAmount = 0;
QString _monthlyCurrency;
mtpRequestId _giftCodeRequestId = 0;
QString _giftCodeSlug;
base::flat_map<QString, GiftCode> _giftCodes;
rpl::event_stream<QString> _giftCodeUpdated;
mtpRequestId _giveawayInfoRequestId = 0;
PeerData *_giveawayInfoPeer = nullptr;
MsgId _giveawayInfoMessageId = 0;
Fn<void(GiveawayInfo)> _giveawayInfoDone;
Data::PremiumSubscriptionOptions _subscriptionOptions;
rpl::event_stream<> _someMessageMoneyRestrictionsResolved;
base::flat_set<not_null<UserData*>> _resolveMessageMoneyRequiredUsers;
base::flat_set<not_null<UserData*>> _resolveMessageMoneyRequestedUsers;
bool _messageMoneyRequestScheduled = false;
};
class PremiumGiftCodeOptions final {
public:
PremiumGiftCodeOptions(not_null<PeerData*> peer);
[[nodiscard]] rpl::producer<rpl::no_value, QString> request();
[[nodiscard]] std::vector<GiftOptionData> optionsForPeer() const;
[[nodiscard]] Data::PremiumSubscriptionOptions optionsForGiveaway(
int usersCount);
[[nodiscard]] const std::vector<int> &availablePresets() const;
[[nodiscard]] int monthsFromPreset(int monthsIndex);
[[nodiscard]] Payments::InvoicePremiumGiftCode invoice(
int users,
int months);
[[nodiscard]] rpl::producer<rpl::no_value, QString> applyPrepaid(
const Payments::InvoicePremiumGiftCode &invoice,
uint64 prepaidId);
[[nodiscard]] int giveawayBoostsPerPremium() const;
[[nodiscard]] int giveawayCountriesMax() const;
[[nodiscard]] int giveawayAddPeersMax() const;
[[nodiscard]] int giveawayPeriodMax() const;
[[nodiscard]] bool giveawayGiftsPurchaseAvailable() const;
[[nodiscard]] rpl::producer<rpl::no_value, QString> requestStarGifts();
[[nodiscard]] const std::vector<Data::StarGift> &starGifts() const;
private:
struct Token final {
int users = 0;
int months = 0;
friend inline constexpr auto operator<=>(Token, Token) = default;
};
struct Store final {
uint64 amount = 0;
QString currency;
QString product;
int quantity = 0;
};
using Amount = int;
using PremiumSubscriptionOptions = Data::PremiumSubscriptionOptions;
const not_null<PeerData*> _peer;
base::flat_map<Amount, PremiumSubscriptionOptions> _subscriptionOptions;
struct {
std::vector<int> months;
std::vector<int64> totalCosts;
std::vector<QString> currencies;
} _optionsForOnePerson;
std::vector<int> _availablePresets;
base::flat_map<Token, Store> _stores;
int32 _giftsHash = 0;
std::vector<Data::StarGift> _gifts;
MTP::Sender _api;
};
class SponsoredToggle final {
public:
explicit SponsoredToggle(not_null<Main::Session*> session);
[[nodiscard]] rpl::producer<bool> toggled();
[[nodiscard]] rpl::producer<rpl::no_value, QString> setToggled(bool);
private:
MTP::Sender _api;
};
struct MessageMoneyRestriction {
int starsPerMessage = 0;
bool premiumRequired = false;
bool known = false;
explicit operator bool() const {
return starsPerMessage != 0 || premiumRequired;
}
friend inline bool operator==(
const MessageMoneyRestriction &,
const MessageMoneyRestriction &) = default;
};
[[nodiscard]] MessageMoneyRestriction ResolveMessageMoneyRestrictions(
not_null<PeerData*> peer,
History *maybeHistory);
[[nodiscard]] rpl::producer<DocumentData*> RandomHelloStickerValue(
not_null<Main::Session*> session);
[[nodiscard]] std::optional<Data::StarGift> FromTL(
not_null<Main::Session*> session,
const MTPstarGift &gift);
[[nodiscard]] std::optional<Data::SavedStarGift> FromTL(
not_null<PeerData*> to,
const MTPsavedStarGift &gift);
[[nodiscard]] Data::UniqueGiftModel FromTL(
not_null<Main::Session*> session,
const MTPDstarGiftAttributeModel &data);
[[nodiscard]] Data::UniqueGiftPattern FromTL(
not_null<Main::Session*> session,
const MTPDstarGiftAttributePattern &data);
[[nodiscard]] Data::UniqueGiftBackdrop FromTL(
const MTPDstarGiftAttributeBackdrop &data);
[[nodiscard]] Data::UniqueGiftOriginalDetails FromTL(
not_null<Main::Session*> session,
const MTPDstarGiftAttributeOriginalDetails &data);
} // namespace Api

View File

@@ -0,0 +1,46 @@
/*
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 "api/api_premium_option.h"
#include "ui/text/format_values.h"
namespace Api {
constexpr auto kDiscountDivider = 1.;
Data::PremiumSubscriptionOption CreateSubscriptionOption(
int months,
int monthlyAmount,
int64 amount,
const QString &currency,
const QString &botUrl) {
const auto discount = [&] {
const auto percent = 1. - float64(amount) / (monthlyAmount * months);
return std::round(percent * 100. / kDiscountDivider)
* kDiscountDivider;
}();
return {
.months = months,
.duration = Ui::FormatTTL(months * 86400 * 31),
.discount = (discount > 0)
? QString::fromUtf8("\xe2\x88\x92%1%").arg(discount)
: QString(),
.costPerMonth = Ui::FillAmountAndCurrency(
amount / float64(months),
currency),
.costNoDiscount = Ui::FillAmountAndCurrency(
monthlyAmount * months,
currency),
.costPerYear = Ui::FillAmountAndCurrency(
amount / float64(months / 12.),
currency),
.botUrl = botUrl,
};
}
} // namespace Api

View File

@@ -0,0 +1,67 @@
/*
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
*/
#pragma once
#include "data/data_premium_subscription_option.h"
namespace Api {
[[nodiscard]] Data::PremiumSubscriptionOption CreateSubscriptionOption(
int months,
int monthlyAmount,
int64 amount,
const QString &currency,
const QString &botUrl);
template<typename Option>
[[nodiscard]] auto PremiumSubscriptionOptionsFromTL(
const QVector<Option> &tlOpts) -> Data::PremiumSubscriptionOptions {
if (tlOpts.isEmpty()) {
return {};
}
auto monthlyAmountPerCurrency = base::flat_map<QString, int>();
auto result = Data::PremiumSubscriptionOptions();
const auto monthlyAmount = [&](const QString &currency) -> int {
const auto it = monthlyAmountPerCurrency.find(currency);
if (it != end(monthlyAmountPerCurrency)) {
return it->second;
}
const auto &min = ranges::min_element(
tlOpts,
ranges::less(),
[&](const Option &o) {
return currency == qs(o.data().vcurrency())
? o.data().vamount().v
: std::numeric_limits<int64_t>::max();
}
)->data();
const auto monthly = min.vamount().v / float64(min.vmonths().v);
monthlyAmountPerCurrency.emplace(currency, monthly);
return monthly;
};
result.reserve(tlOpts.size());
for (const auto &tlOption : tlOpts) {
const auto &option = tlOption.data();
auto botUrl = QString();
if constexpr (!std::is_same_v<Option, MTPPremiumGiftCodeOption>) {
botUrl = qs(option.vbot_url());
}
const auto months = option.vmonths().v;
const auto amount = option.vamount().v;
const auto currency = qs(option.vcurrency());
result.push_back(CreateSubscriptionOption(
months,
monthlyAmount(currency),
amount,
currency,
botUrl));
}
return result;
}
} // namespace Api

View File

@@ -0,0 +1,145 @@
/*
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 "api/api_report.h"
#include "apiwrap.h"
#include "data/data_peer.h"
#include "data/data_photo.h"
#include "data/data_report.h"
#include "data/data_user.h"
#include "lang/lang_keys.h"
#include "main/main_session.h"
#include "ui/boxes/report_box_graphics.h"
#include "ui/layers/show.h"
namespace Api {
namespace {
MTPreportReason ReasonToTL(const Ui::ReportReason &reason) {
using Reason = Ui::ReportReason;
switch (reason) {
case Reason::Spam: return MTP_inputReportReasonSpam();
case Reason::Fake: return MTP_inputReportReasonFake();
case Reason::Violence: return MTP_inputReportReasonViolence();
case Reason::ChildAbuse: return MTP_inputReportReasonChildAbuse();
case Reason::Pornography: return MTP_inputReportReasonPornography();
case Reason::Copyright: return MTP_inputReportReasonCopyright();
case Reason::IllegalDrugs: return MTP_inputReportReasonIllegalDrugs();
case Reason::PersonalDetails:
return MTP_inputReportReasonPersonalDetails();
case Reason::Other: return MTP_inputReportReasonOther();
}
Unexpected("Bad reason group value.");
}
} // namespace
void SendPhotoReport(
std::shared_ptr<Ui::Show> show,
not_null<PeerData*> peer,
Ui::ReportReason reason,
const QString &comment,
not_null<PhotoData*> photo) {
peer->session().api().request(MTPaccount_ReportProfilePhoto(
peer->input(),
photo->mtpInput(),
ReasonToTL(reason),
MTP_string(comment)
)).done([=] {
show->showToast(tr::lng_report_thanks(tr::now));
}).send();
}
auto CreateReportMessagesOrStoriesCallback(
std::shared_ptr<Ui::Show> show,
not_null<PeerData*> peer)
-> Fn<void(Data::ReportInput, Fn<void(ReportResult)>)> {
using TLChoose = MTPDreportResultChooseOption;
using TLAddComment = MTPDreportResultAddComment;
using TLReported = MTPDreportResultReported;
using Result = ReportResult;
struct State final {
#ifdef _DEBUG
~State() {
qDebug() << "Messages or Stories Report ~State().";
}
#endif
mtpRequestId requestId = 0;
};
const auto state = std::make_shared<State>();
return [=](
Data::ReportInput reportInput,
Fn<void(Result)> done) {
auto apiIds = QVector<MTPint>();
apiIds.reserve(reportInput.ids.size() + reportInput.stories.size());
for (const auto &id : reportInput.ids) {
apiIds.push_back(MTP_int(id));
}
for (const auto &story : reportInput.stories) {
apiIds.push_back(MTP_int(story));
}
const auto received = [=](
const MTPReportResult &result,
mtpRequestId requestId) {
if (state->requestId != requestId) {
return;
}
state->requestId = 0;
done(result.match([&](const TLChoose &data) {
const auto t = qs(data.vtitle());
auto list = Result::Options();
list.reserve(data.voptions().v.size());
for (const auto &tl : data.voptions().v) {
list.emplace_back(Result::Option{
.id = tl.data().voption().v,
.text = qs(tl.data().vtext()),
});
}
return Result{ .options = std::move(list), .title = t };
}, [&](const TLAddComment &data) -> Result {
return {
.commentOption = ReportResult::CommentOption{
.optional = data.is_optional(),
.id = data.voption().v,
}
};
}, [&](const TLReported &data) -> Result {
return { .successful = true };
}));
};
const auto fail = [=](const MTP::Error &error) {
state->requestId = 0;
done({ .error = error.type() });
};
if (!reportInput.stories.empty()) {
state->requestId = peer->session().api().request(
MTPstories_Report(
peer->input(),
MTP_vector<MTPint>(apiIds),
MTP_bytes(reportInput.optionId),
MTP_string(reportInput.comment))
).done(received).fail(fail).send();
} else {
state->requestId = peer->session().api().request(
MTPmessages_Report(
peer->input(),
MTP_vector<MTPint>(apiIds),
MTP_bytes(reportInput.optionId),
MTP_string(reportInput.comment))
).done(received).fail(fail).send();
}
};
}
} // namespace Api

View File

@@ -0,0 +1,56 @@
/*
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
*/
#pragma once
class HistoryItem;
class PeerData;
class PhotoData;
namespace Ui {
class Show;
enum class ReportReason;
} // namespace Ui
namespace Data {
struct ReportInput;
} // namespace Data
namespace Api {
struct ReportResult final {
using Id = QByteArray;
struct Option final {
Id id = 0;
QString text;
};
using Options = std::vector<Option>;
Options options;
QString title;
QString error;
QString comment;
struct CommentOption {
bool optional = false;
Id id = 0;
};
std::optional<CommentOption> commentOption;
bool successful = false;
};
void SendPhotoReport(
std::shared_ptr<Ui::Show> show,
not_null<PeerData*> peer,
Ui::ReportReason reason,
const QString &comment,
not_null<PhotoData*> photo);
[[nodiscard]] auto CreateReportMessagesOrStoriesCallback(
std::shared_ptr<Ui::Show> show,
not_null<PeerData*> peer)
-> Fn<void(Data::ReportInput, Fn<void(ReportResult)>)>;
} // namespace Api

View File

@@ -0,0 +1,205 @@
/*
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 "api/api_ringtones.h"
#include "api/api_toggling_media.h"
#include "apiwrap.h"
#include "base/random.h"
#include "base/unixtime.h"
#include "data/data_document.h"
#include "data/data_document_media.h"
#include "data/data_file_origin.h"
#include "data/data_session.h"
#include "data/notify/data_notify_settings.h"
#include "main/main_app_config.h"
#include "main/main_session.h"
#include "storage/file_upload.h"
#include "storage/localimageloader.h"
namespace Api {
namespace {
std::shared_ptr<FilePrepareResult> PrepareRingtoneDocument(
MTP::DcId dcId,
const QString &filename,
const QString &filemime,
const QByteArray &content) {
const auto id = base::RandomValue<DocumentId>();
auto attributes = QVector<MTPDocumentAttribute>(
1,
MTP_documentAttributeFilename(MTP_string(filename)));
auto result = MakePreparedFile({
.id = id,
.type = SendMediaType::File,
});
result->filename = filename;
result->content = content;
result->filesize = content.size();
result->setFileData(content);
result->document = MTP_document(
MTP_flags(0),
MTP_long(id),
MTP_long(0),
MTP_bytes(),
MTP_int(base::unixtime::now()),
MTP_string(filemime),
MTP_long(content.size()),
MTP_vector<MTPPhotoSize>(),
MTPVector<MTPVideoSize>(),
MTP_int(dcId),
MTP_vector<MTPDocumentAttribute>(std::move(attributes)));
return result;
}
} // namespace
Ringtones::Ringtones(not_null<ApiWrap*> api)
: _session(&api->session())
, _api(&api->instance()) {
crl::on_main(_session, [=] {
// You can't use _session->lifetime() in the constructor,
// only queued, because it is not constructed yet.
_session->uploader().documentReady(
) | rpl::on_next([=](const Storage::UploadedMedia &data) {
ready(data.fullId, data.info.file);
}, _session->lifetime());
});
}
void Ringtones::upload(
const QString &filename,
const QString &filemime,
const QByteArray &content) {
const auto ready = PrepareRingtoneDocument(
_api.instance().mainDcId(),
filename,
filemime,
content);
const auto uploadedData = UploadedData{ filename, filemime, content };
const auto fakeId = FullMsgId(
_session->userPeerId(),
_session->data().nextLocalMessageId());
const auto already = ranges::find_if(
_uploads,
[&](const auto &d) {
return uploadedData.filemime == d.second.filemime
&& uploadedData.filename == d.second.filename;
});
if (already != end(_uploads)) {
_session->uploader().cancel(already->first);
_uploads.erase(already);
}
_uploads.emplace(fakeId, uploadedData);
_session->uploader().upload(fakeId, ready);
}
void Ringtones::ready(const FullMsgId &msgId, const MTPInputFile &file) {
const auto maybeUploadedData = _uploads.take(msgId);
if (!maybeUploadedData) {
return;
}
const auto uploadedData = *maybeUploadedData;
_api.request(MTPaccount_UploadRingtone(
file,
MTP_string(uploadedData.filename),
MTP_string(uploadedData.filemime)
)).done([=, content = uploadedData.content](const MTPDocument &result) {
const auto document = _session->data().processDocument(result);
_list.documents.insert(_list.documents.begin(), document->id);
const auto media = document->createMediaView();
media->setBytes(content);
document->owner().notifySettings().cacheSound(document);
_uploadDones.fire_copy(document->id);
}).fail([=](const MTP::Error &error) {
_uploadFails.fire_copy(error.type());
}).send();
}
void Ringtones::requestList() {
if (_list.requestId) {
return;
}
_list.requestId = _api.request(
MTPaccount_GetSavedRingtones(MTP_long(_list.hash))
).done([=](const MTPaccount_SavedRingtones &result) {
_list.requestId = 0;
result.match([&](const MTPDaccount_savedRingtones &data) {
_list.hash = data.vhash().v;
_list.documents.clear();
_list.documents.reserve(data.vringtones().v.size());
for (const auto &d : data.vringtones().v) {
const auto document = _session->data().processDocument(d);
document->forceToCache(true);
_list.documents.emplace_back(document->id);
}
_list.updates.fire({});
}, [&](const MTPDaccount_savedRingtonesNotModified &) {
});
}).fail([=] {
_list.requestId = 0;
}).send();
}
const Ringtones::Ids &Ringtones::list() const {
return _list.documents;
}
rpl::producer<> Ringtones::listUpdates() const {
return _list.updates.events();
}
rpl::producer<QString> Ringtones::uploadFails() const {
return _uploadFails.events();
}
rpl::producer<DocumentId> Ringtones::uploadDones() const {
return _uploadDones.events();
}
void Ringtones::applyUpdate() {
_list.hash = 0;
_list.documents.clear();
requestList();
}
void Ringtones::remove(DocumentId id) {
if (const auto document = _session->data().document(id)) {
ToggleSavedRingtone(
document,
Data::FileOriginRingtones(),
crl::guard(&document->session(), [=] {
const auto it = ranges::find(_list.documents, id);
if (it != end(_list.documents)) {
_list.documents.erase(it);
}
}),
false);
}
}
int64 Ringtones::maxSize() const {
return _session->appConfig().get<int>(
u"ringtone_size_max"_q,
100 * 1024);
}
int Ringtones::maxSavedCount() const {
return _session->appConfig().get<int>(
u"ringtone_saved_count_max"_q,
100);
}
crl::time Ringtones::maxDuration() const {
return crl::time(1000) * _session->appConfig().get<int>(
u"ringtone_duration_max"_q,
5);
}
} // namespace Api

View File

@@ -0,0 +1,69 @@
/*
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
*/
#pragma once
#include "mtproto/sender.h"
class ApiWrap;
class PeerData;
namespace Main {
class Session;
} // namespace Main
namespace Api {
class Ringtones final {
public:
explicit Ringtones(not_null<ApiWrap*> api);
using Ids = std::vector<DocumentId>;
void requestList();
void applyUpdate();
void remove(DocumentId id);
void upload(
const QString &filename,
const QString &filemime,
const QByteArray &content);
[[nodiscard]] const Ids &list() const;
[[nodiscard]] rpl::producer<> listUpdates() const;
[[nodiscard]] rpl::producer<QString> uploadFails() const;
[[nodiscard]] rpl::producer<DocumentId> uploadDones() const;
[[nodiscard]] int64 maxSize() const;
[[nodiscard]] int maxSavedCount() const;
[[nodiscard]] crl::time maxDuration() const;
private:
struct UploadedData {
QString filename;
QString filemime;
QByteArray content;
};
void ready(const FullMsgId &msgId, const MTPInputFile &file);
const not_null<Main::Session*> _session;
MTP::Sender _api;
base::flat_map<FullMsgId, UploadedData> _uploads;
rpl::event_stream<QString> _uploadFails;
rpl::event_stream<DocumentId> _uploadDones;
struct {
uint64 hash = 0;
Ids documents;
rpl::event_stream<> updates;
mtpRequestId requestId = 0;
} _list;
};
} // namespace Api

View File

@@ -0,0 +1,76 @@
/*
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 "api/api_self_destruct.h"
#include "apiwrap.h"
namespace Api {
SelfDestruct::SelfDestruct(not_null<ApiWrap*> api)
: _api(&api->instance()) {
}
void SelfDestruct::reload() {
if (!_accountTTL.requestId) {
_accountTTL.requestId = _api.request(MTPaccount_GetAccountTTL(
)).done([=](const MTPAccountDaysTTL &result) {
_accountTTL.requestId = 0;
_accountTTL.days = result.data().vdays().v;
}).fail([=] {
_accountTTL.requestId = 0;
}).send();
}
if (!_defaultHistoryTTL.requestId) {
_defaultHistoryTTL.requestId = _api.request(
MTPmessages_GetDefaultHistoryTTL()
).done([=](const MTPDefaultHistoryTTL &result) {
_defaultHistoryTTL.requestId = 0;
_defaultHistoryTTL.period = result.data().vperiod().v;
}).fail([=] {
_defaultHistoryTTL.requestId = 0;
}).send();
}
}
rpl::producer<int> SelfDestruct::daysAccountTTL() const {
return _accountTTL.days.value() | rpl::filter(rpl::mappers::_1 != 0);
}
rpl::producer<TimeId> SelfDestruct::periodDefaultHistoryTTL() const {
return _defaultHistoryTTL.period.value();
}
TimeId SelfDestruct::periodDefaultHistoryTTLCurrent() const {
return _defaultHistoryTTL.period.current();
}
void SelfDestruct::updateAccountTTL(int days) {
_api.request(_accountTTL.requestId).cancel();
_accountTTL.requestId = _api.request(MTPaccount_SetAccountTTL(
MTP_accountDaysTTL(MTP_int(days))
)).done([=] {
_accountTTL.requestId = 0;
}).fail([=] {
_accountTTL.requestId = 0;
}).send();
_accountTTL.days = days;
}
void SelfDestruct::updateDefaultHistoryTTL(TimeId period) {
_api.request(_defaultHistoryTTL.requestId).cancel();
_defaultHistoryTTL.requestId = _api.request(
MTPmessages_SetDefaultHistoryTTL(MTP_int(period))
).done([=] {
_defaultHistoryTTL.requestId = 0;
}).fail([=] {
_defaultHistoryTTL.requestId = 0;
}).send();
_defaultHistoryTTL.period = period;
}
} // namespace Api

View File

@@ -0,0 +1,42 @@
/*
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
*/
#pragma once
#include "mtproto/sender.h"
class ApiWrap;
namespace Api {
class SelfDestruct final {
public:
explicit SelfDestruct(not_null<ApiWrap*> api);
void reload();
void updateAccountTTL(int days);
void updateDefaultHistoryTTL(TimeId period);
[[nodiscard]] rpl::producer<int> daysAccountTTL() const;
[[nodiscard]] rpl::producer<TimeId> periodDefaultHistoryTTL() const;
[[nodiscard]] TimeId periodDefaultHistoryTTLCurrent() const;
private:
MTP::Sender _api;
struct {
mtpRequestId requestId = 0;
rpl::variable<int> days = 0;
} _accountTTL;
struct {
mtpRequestId requestId = 0;
rpl::variable<TimeId> period = 0;
} _defaultHistoryTTL;
};
} // namespace Api

View File

@@ -0,0 +1,182 @@
/*
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 "api/api_send_progress.h"
#include "main/main_session.h"
#include "history/history.h"
#include "data/data_peer.h"
#include "data/data_user.h"
#include "base/unixtime.h"
#include "data/data_peer_values.h"
#include "apiwrap.h"
namespace Api {
namespace {
constexpr auto kCancelTypingActionTimeout = crl::time(5000);
constexpr auto kSendMySpeakingInterval = 3 * crl::time(1000);
constexpr auto kSendMyTypingInterval = 5 * crl::time(1000);
constexpr auto kSendTypingsToOfflineFor = TimeId(30);
} // namespace
SendProgressManager::SendProgressManager(not_null<Main::Session*> session)
: _session(session)
, _stopTypingTimer([=] { cancelTyping(base::take(_stopTypingHistory)); }) {
}
void SendProgressManager::cancel(
not_null<History*> history,
SendProgressType type) {
cancel(history, 0, type);
}
void SendProgressManager::cancel(
not_null<History*> history,
MsgId topMsgId,
SendProgressType type) {
const auto i = _requests.find(Key{ history, topMsgId, type });
if (i != _requests.end()) {
_session->api().request(i->second).cancel();
_requests.erase(i);
}
}
void SendProgressManager::cancelTyping(not_null<History*> history) {
_stopTypingTimer.cancel();
cancel(history, SendProgressType::Typing);
}
void SendProgressManager::update(
not_null<History*> history,
SendProgressType type,
int progress) {
update(history, 0, type, progress);
}
void SendProgressManager::update(
not_null<History*> history,
MsgId topMsgId,
SendProgressType type,
int progress) {
const auto peer = history->peer;
if (peer->isSelf()
|| (peer->isChannel()
&& !peer->isMegagroup()
&& type != SendProgressType::Speaking)) {
return;
}
const auto doing = (progress >= 0);
const auto key = Key{ history, topMsgId, type };
if (updated(key, doing)) {
cancel(history, topMsgId, type);
if (doing) {
send(key, progress);
}
}
}
bool SendProgressManager::updated(const Key &key, bool doing) {
const auto now = crl::now();
const auto i = _updated.find(key);
if (doing) {
const auto sendEach = (key.type == SendProgressType::Speaking)
? kSendMySpeakingInterval
: kSendMyTypingInterval;
if (i == end(_updated)) {
_updated.emplace(key, now + 2 * sendEach);
} else if (i->second > now + sendEach) {
return false;
} else {
i->second = now + 2 * sendEach;
}
} else {
if (i == end(_updated)) {
return false;
} else if (i->second <= now) {
return false;
} else {
_updated.erase(i);
}
}
return true;
}
void SendProgressManager::send(const Key &key, int progress) {
if (skipRequest(key)) {
return;
}
using Type = SendProgressType;
const auto action = [&]() -> MTPsendMessageAction {
const auto p = MTP_int(progress);
switch (key.type) {
case Type::Typing: return MTP_sendMessageTypingAction();
case Type::RecordVideo: return MTP_sendMessageRecordVideoAction();
case Type::UploadVideo: return MTP_sendMessageUploadVideoAction(p);
case Type::RecordVoice: return MTP_sendMessageRecordAudioAction();
case Type::UploadVoice: return MTP_sendMessageUploadAudioAction(p);
case Type::RecordRound: return MTP_sendMessageRecordRoundAction();
case Type::UploadRound: return MTP_sendMessageUploadRoundAction(p);
case Type::UploadPhoto: return MTP_sendMessageUploadPhotoAction(p);
case Type::UploadFile: return MTP_sendMessageUploadDocumentAction(p);
case Type::ChooseLocation: return MTP_sendMessageGeoLocationAction();
case Type::ChooseContact: return MTP_sendMessageChooseContactAction();
case Type::PlayGame: return MTP_sendMessageGamePlayAction();
case Type::Speaking: return MTP_speakingInGroupCallAction();
case Type::ChooseSticker: return MTP_sendMessageChooseStickerAction();
default: return MTP_sendMessageTypingAction();
}
}();
const auto requestId = _session->api().request(MTPmessages_SetTyping(
MTP_flags(key.topMsgId
? MTPmessages_SetTyping::Flag::f_top_msg_id
: MTPmessages_SetTyping::Flag(0)),
key.history->peer->input(),
MTP_int(key.topMsgId),
action
)).done([=](const MTPBool &result, mtpRequestId requestId) {
done(requestId);
}).send();
_requests.emplace(key, requestId);
if (key.type == Type::Typing) {
_stopTypingHistory = key.history;
_stopTypingTimer.callOnce(kCancelTypingActionTimeout);
}
}
bool SendProgressManager::skipRequest(const Key &key) const {
const auto user = key.history->peer->asUser();
if (!user) {
return false;
} else if (user->isSelf()) {
return true;
} else if (user->isBot() && !user->isSupport()) {
return true;
}
const auto recently = base::unixtime::now() - kSendTypingsToOfflineFor;
const auto lastseen = user->lastseen();
if (lastseen.isRecently()) {
return false;
} else if (const auto value = lastseen.onlineTill()) {
return (value < recently);
}
return true;
}
void SendProgressManager::done(mtpRequestId requestId) {
for (auto i = _requests.begin(), e = _requests.end(); i != e; ++i) {
if (i->second == requestId) {
_requests.erase(i);
break;
}
}
}
} // namespace Api

View File

@@ -0,0 +1,105 @@
/*
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
*/
#pragma once
#include "api/api_common.h"
#include "base/timer.h"
class History;
namespace Main {
class Session;
} // namespace Main
namespace Api {
enum class SendProgressType {
Typing,
RecordVideo,
UploadVideo,
RecordVoice,
UploadVoice,
RecordRound,
UploadRound,
UploadPhoto,
UploadFile,
ChooseLocation,
ChooseContact,
ChooseSticker,
PlayGame,
Speaking,
};
struct SendProgress {
SendProgress(
SendProgressType type,
crl::time until,
int progress = 0)
: type(type)
, until(until)
, progress(progress) {
}
SendProgressType type = SendProgressType::Typing;
crl::time until = 0;
int progress = 0;
};
class SendProgressManager final {
public:
SendProgressManager(not_null<Main::Session*> session);
void update(
not_null<History*> history,
SendProgressType type,
int progress = 0);
void update(
not_null<History*> history,
MsgId topMsgId,
SendProgressType type,
int progress = 0);
void cancel(
not_null<History*> history,
MsgId topMsgId,
SendProgressType type);
void cancel(
not_null<History*> history,
SendProgressType type);
void cancelTyping(not_null<History*> history);
private:
struct Key {
not_null<History*> history;
MsgId topMsgId = 0;
SendProgressType type = SendProgressType();
inline bool operator<(const Key &other) const {
return (history < other.history)
|| (history == other.history && topMsgId < other.topMsgId)
|| (history == other.history
&& topMsgId == other.topMsgId
&& type < other.type);
}
};
bool updated(const Key &key, bool doing);
void send(const Key &key, int progress);
void done(mtpRequestId requestId);
[[nodiscard]] bool skipRequest(const Key &key) const;
const not_null<Main::Session*> _session;
base::flat_map<Key, mtpRequestId> _requests;
base::flat_map<Key, crl::time> _updated;
base::Timer _stopTypingTimer;
History *_stopTypingHistory = nullptr;
};
} // namespace Api

View File

@@ -0,0 +1,688 @@
/*
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 "api/api_sending.h"
#include "api/api_text_entities.h"
#include "base/random.h"
#include "base/unixtime.h"
#include "data/business/data_shortcut_messages.h"
#include "data/data_document.h"
#include "data/data_photo.h"
#include "data/data_channel.h" // ChannelData::addsSignature.
#include "data/data_user.h" // UserData::name
#include "data/data_session.h"
#include "data/data_file_origin.h"
#include "data/data_histories.h"
#include "data/data_changes.h"
#include "data/stickers/data_stickers.h"
#include "history/history.h"
#include "history/history_item.h"
#include "history/history_item_helpers.h" // NewMessageFlags.
#include "chat_helpers/message_field.h" // ConvertTextTagsToEntities.
#include "chat_helpers/stickers_dice_pack.h" // DicePacks::kDiceString.
#include "ui/text/text_entity.h" // TextWithEntities.
#include "ui/item_text_options.h" // Ui::ItemTextOptions.
#include "main/main_session.h"
#include "main/main_app_config.h"
#include "storage/localimageloader.h"
#include "storage/file_upload.h"
#include "mainwidget.h"
#include "apiwrap.h"
namespace Api {
namespace {
void InnerFillMessagePostFlags(
const SendOptions &options,
not_null<PeerData*> peer,
MessageFlags &flags) {
if (ShouldSendSilent(peer, options)) {
flags |= MessageFlag::Silent;
}
if (!peer->amAnonymous()
|| (!peer->isBroadcast()
&& options.sendAs
&& options.sendAs != peer)) {
flags |= MessageFlag::HasFromId;
}
const auto channel = peer->asBroadcast();
if (!channel) {
return;
}
flags |= MessageFlag::Post;
// Don't display views and author of a new post when it's scheduled.
if (options.scheduled) {
return;
}
flags |= MessageFlag::HasViews;
if (channel->addsSignature()) {
flags |= MessageFlag::HasPostAuthor;
}
}
void SendSimpleMedia(SendAction action, MTPInputMedia inputMedia) {
const auto history = action.history;
const auto peer = history->peer;
const auto session = &history->session();
const auto api = &session->api();
action.clearDraft = false;
action.generateLocal = false;
api->sendAction(action);
const auto randomId = base::RandomValue<uint64>();
auto flags = NewMessageFlags(peer);
auto sendFlags = MTPmessages_SendMedia::Flags(0);
if (action.replyTo) {
flags |= MessageFlag::HasReplyInfo;
sendFlags |= MTPmessages_SendMedia::Flag::f_reply_to;
}
const auto silentPost = ShouldSendSilent(peer, action.options);
InnerFillMessagePostFlags(action.options, peer, flags);
if (silentPost) {
sendFlags |= MTPmessages_SendMedia::Flag::f_silent;
}
const auto sendAs = action.options.sendAs;
if (sendAs) {
sendFlags |= MTPmessages_SendMedia::Flag::f_send_as;
}
const auto messagePostAuthor = peer->isBroadcast()
? session->user()->name()
: QString();
const auto starsPaid = std::min(
peer->starsPerMessageChecked(),
action.options.starsApproved);
if (action.options.scheduled) {
flags |= MessageFlag::IsOrWasScheduled;
sendFlags |= MTPmessages_SendMedia::Flag::f_schedule_date;
if (action.options.scheduleRepeatPeriod) {
sendFlags |= MTPmessages_SendMedia::Flag::f_schedule_repeat_period;
}
}
if (action.options.shortcutId) {
flags |= MessageFlag::ShortcutMessage;
sendFlags |= MTPmessages_SendMedia::Flag::f_quick_reply_shortcut;
}
if (action.options.effectId) {
sendFlags |= MTPmessages_SendMedia::Flag::f_effect;
}
if (action.options.suggest) {
sendFlags |= MTPmessages_SendMedia::Flag::f_suggested_post;
}
if (action.options.invertCaption) {
flags |= MessageFlag::InvertMedia;
sendFlags |= MTPmessages_SendMedia::Flag::f_invert_media;
}
if (starsPaid) {
action.options.starsApproved -= starsPaid;
sendFlags |= MTPmessages_SendMedia::Flag::f_allow_paid_stars;
}
auto &histories = history->owner().histories();
histories.sendPreparedMessage(
history,
action.replyTo,
randomId,
Data::Histories::PrepareMessage<MTPmessages_SendMedia>(
MTP_flags(sendFlags),
peer->input(),
Data::Histories::ReplyToPlaceholder(),
std::move(inputMedia),
MTPstring(),
MTP_long(randomId),
MTPReplyMarkup(),
MTPvector<MTPMessageEntity>(),
MTP_int(action.options.scheduled),
MTP_int(action.options.scheduleRepeatPeriod),
(sendAs ? sendAs->input() : MTP_inputPeerEmpty()),
Data::ShortcutIdToMTP(session, action.options.shortcutId),
MTP_long(action.options.effectId),
MTP_long(starsPaid),
SuggestToMTP(action.options.suggest)
), [=](const MTPUpdates &result, const MTP::Response &response) {
}, [=](const MTP::Error &error, const MTP::Response &response) {
api->sendMessageFail(error, peer, randomId);
});
api->finishForwarding(action);
}
template <typename MediaData>
void SendExistingMedia(
MessageToSend &&message,
not_null<MediaData*> media,
Fn<MTPInputMedia()> inputMedia,
Data::FileOrigin origin,
std::optional<MsgId> localMessageId) {
const auto history = message.action.history;
const auto peer = history->peer;
const auto session = &history->session();
const auto api = &session->api();
message.action.clearDraft = false;
message.action.generateLocal = true;
api->sendAction(message.action);
const auto newId = FullMsgId(
peer->id,
localMessageId
? (*localMessageId)
: session->data().nextLocalMessageId());
const auto randomId = base::RandomValue<uint64>();
auto &action = message.action;
auto flags = NewMessageFlags(peer);
auto sendFlags = MTPmessages_SendMedia::Flags(0);
if (action.replyTo) {
flags |= MessageFlag::HasReplyInfo;
sendFlags |= MTPmessages_SendMedia::Flag::f_reply_to;
}
const auto silentPost = ShouldSendSilent(peer, action.options);
InnerFillMessagePostFlags(action.options, peer, flags);
if (silentPost) {
sendFlags |= MTPmessages_SendMedia::Flag::f_silent;
}
const auto sendAs = action.options.sendAs;
if (sendAs) {
sendFlags |= MTPmessages_SendMedia::Flag::f_send_as;
}
auto caption = TextWithEntities{
message.textWithTags.text,
TextUtilities::ConvertTextTagsToEntities(message.textWithTags.tags)
};
TextUtilities::Trim(caption);
auto sentEntities = EntitiesToMTP(
session,
caption.entities,
ConvertOption::SkipLocal);
if (!sentEntities.v.isEmpty()) {
sendFlags |= MTPmessages_SendMedia::Flag::f_entities;
}
const auto captionText = caption.text;
const auto starsPaid = std::min(
peer->starsPerMessageChecked(),
action.options.starsApproved);
if (action.options.scheduled) {
flags |= MessageFlag::IsOrWasScheduled;
sendFlags |= MTPmessages_SendMedia::Flag::f_schedule_date;
if (action.options.scheduleRepeatPeriod) {
sendFlags |= MTPmessages_SendMedia::Flag::f_schedule_repeat_period;
}
}
if (action.options.shortcutId) {
flags |= MessageFlag::ShortcutMessage;
sendFlags |= MTPmessages_SendMedia::Flag::f_quick_reply_shortcut;
}
if (action.options.effectId) {
sendFlags |= MTPmessages_SendMedia::Flag::f_effect;
}
if (action.options.suggest) {
sendFlags |= MTPmessages_SendMedia::Flag::f_suggested_post;
}
if (action.options.invertCaption) {
flags |= MessageFlag::InvertMedia;
sendFlags |= MTPmessages_SendMedia::Flag::f_invert_media;
}
if (starsPaid) {
action.options.starsApproved -= starsPaid;
sendFlags |= MTPmessages_SendMedia::Flag::f_allow_paid_stars;
}
session->data().registerMessageRandomId(randomId, newId);
history->addNewLocalMessage({
.id = newId.msg,
.flags = flags,
.from = NewMessageFromId(action),
.replyTo = action.replyTo,
.date = NewMessageDate(action.options),
.shortcutId = action.options.shortcutId,
.starsPaid = starsPaid,
.postAuthor = NewMessagePostAuthor(action),
.effectId = action.options.effectId,
.suggest = HistoryMessageSuggestInfo(action.options),
}, media, caption);
const auto performRequest = [=](const auto &repeatRequest) -> void {
auto &histories = history->owner().histories();
const auto session = &history->session();
const auto usedFileReference = media->fileReference();
histories.sendPreparedMessage(
history,
action.replyTo,
randomId,
Data::Histories::PrepareMessage<MTPmessages_SendMedia>(
MTP_flags(sendFlags),
peer->input(),
Data::Histories::ReplyToPlaceholder(),
inputMedia(),
MTP_string(captionText),
MTP_long(randomId),
MTPReplyMarkup(),
sentEntities,
MTP_int(action.options.scheduled),
MTP_int(action.options.scheduleRepeatPeriod),
(sendAs ? sendAs->input() : MTP_inputPeerEmpty()),
Data::ShortcutIdToMTP(session, action.options.shortcutId),
MTP_long(action.options.effectId),
MTP_long(starsPaid),
SuggestToMTP(action.options.suggest)
), [=](const MTPUpdates &result, const MTP::Response &response) {
}, [=](const MTP::Error &error, const MTP::Response &response) {
if (error.code() == 400
&& error.type().startsWith(u"FILE_REFERENCE_"_q)) {
api->refreshFileReference(origin, [=](const auto &result) {
if (media->fileReference() != usedFileReference) {
repeatRequest(repeatRequest);
} else {
api->sendMessageFail(error, peer, randomId, newId);
}
});
} else {
api->sendMessageFail(error, peer, randomId, newId);
}
});
};
performRequest(performRequest);
api->finishForwarding(action);
}
} // namespace
void SendExistingDocument(
MessageToSend &&message,
not_null<DocumentData*> document,
std::optional<MsgId> localMessageId) {
const auto inputMedia = [=] {
return MTP_inputMediaDocument(
MTP_flags(0),
document->mtpInput(),
MTPInputPhoto(), // video_cover
MTPint(), // ttl_seconds
MTPint(), // video_timestamp
MTPstring()); // query
};
SendExistingMedia(
std::move(message),
document,
inputMedia,
document->stickerOrGifOrigin(),
std::move(localMessageId));
if (document->sticker()) {
document->owner().stickers().incrementSticker(document);
}
}
void SendExistingPhoto(
MessageToSend &&message,
not_null<PhotoData*> photo,
std::optional<MsgId> localMessageId) {
const auto inputMedia = [=] {
return MTP_inputMediaPhoto(
MTP_flags(0),
photo->mtpInput(),
MTPint());
};
SendExistingMedia(
std::move(message),
photo,
inputMedia,
Data::FileOrigin(),
std::move(localMessageId));
}
bool SendDice(MessageToSend &message) {
const auto full = QStringView(message.textWithTags.text).trimmed();
auto length = 0;
if (!Ui::Emoji::Find(full.data(), full.data() + full.size(), &length)
|| length != full.size()
|| !message.textWithTags.tags.isEmpty()) {
return false;
}
auto &config = message.action.history->session().appConfig();
static const auto hardcoded = std::vector<QString>{
Stickers::DicePacks::kDiceString,
Stickers::DicePacks::kDartString,
Stickers::DicePacks::kSlotString,
Stickers::DicePacks::kFballString,
Stickers::DicePacks::kFballString + QChar(0xFE0F),
Stickers::DicePacks::kBballString,
};
const auto list = config.get<std::vector<QString>>(
"emojies_send_dice",
hardcoded);
const auto emoji = full.toString();
if (!ranges::contains(list, emoji)) {
return false;
}
const auto history = message.action.history;
const auto peer = history->peer;
const auto session = &history->session();
const auto api = &session->api();
message.textWithTags = TextWithTags();
message.action.clearDraft = false;
message.action.generateLocal = true;
auto &action = message.action;
api->sendAction(action);
const auto newId = FullMsgId(
peer->id,
session->data().nextLocalMessageId());
const auto randomId = base::RandomValue<uint64>();
auto &histories = history->owner().histories();
auto flags = NewMessageFlags(peer);
auto sendFlags = MTPmessages_SendMedia::Flags(0);
if (action.replyTo) {
flags |= MessageFlag::HasReplyInfo;
sendFlags |= MTPmessages_SendMedia::Flag::f_reply_to;
}
const auto silentPost = ShouldSendSilent(peer, action.options);
InnerFillMessagePostFlags(action.options, peer, flags);
if (silentPost) {
sendFlags |= MTPmessages_SendMedia::Flag::f_silent;
}
const auto sendAs = action.options.sendAs;
if (sendAs) {
sendFlags |= MTPmessages_SendMedia::Flag::f_send_as;
}
if (action.options.scheduled) {
flags |= MessageFlag::IsOrWasScheduled;
sendFlags |= MTPmessages_SendMedia::Flag::f_schedule_date;
if (action.options.scheduleRepeatPeriod) {
sendFlags |= MTPmessages_SendMedia::Flag::f_schedule_repeat_period;
}
}
if (action.options.shortcutId) {
flags |= MessageFlag::ShortcutMessage;
sendFlags |= MTPmessages_SendMedia::Flag::f_quick_reply_shortcut;
}
if (action.options.effectId) {
sendFlags |= MTPmessages_SendMedia::Flag::f_effect;
}
if (action.options.suggest) {
sendFlags |= MTPmessages_SendMedia::Flag::f_suggested_post;
}
if (action.options.invertCaption) {
flags |= MessageFlag::InvertMedia;
sendFlags |= MTPmessages_SendMedia::Flag::f_invert_media;
}
const auto starsPaid = std::min(
peer->starsPerMessageChecked(),
action.options.starsApproved);
if (starsPaid) {
action.options.starsApproved -= starsPaid;
sendFlags |= MTPmessages_SendMedia::Flag::f_allow_paid_stars;
}
session->data().registerMessageRandomId(randomId, newId);
history->addNewLocalMessage({
.id = newId.msg,
.flags = flags,
.from = NewMessageFromId(action),
.replyTo = action.replyTo,
.date = NewMessageDate(action.options),
.shortcutId = action.options.shortcutId,
.starsPaid = starsPaid,
.postAuthor = NewMessagePostAuthor(action),
.effectId = action.options.effectId,
.suggest = HistoryMessageSuggestInfo(action.options),
}, TextWithEntities(), MTP_messageMediaDice(
MTP_int(0),
MTP_string(emoji)));
histories.sendPreparedMessage(
history,
action.replyTo,
randomId,
Data::Histories::PrepareMessage<MTPmessages_SendMedia>(
MTP_flags(sendFlags),
peer->input(),
Data::Histories::ReplyToPlaceholder(),
MTP_inputMediaDice(MTP_string(emoji)),
MTP_string(),
MTP_long(randomId),
MTPReplyMarkup(),
MTP_vector<MTPMessageEntity>(),
MTP_int(action.options.scheduled),
MTP_int(action.options.scheduleRepeatPeriod),
(sendAs ? sendAs->input() : MTP_inputPeerEmpty()),
Data::ShortcutIdToMTP(session, action.options.shortcutId),
MTP_long(action.options.effectId),
MTP_long(starsPaid),
SuggestToMTP(action.options.suggest)
), [=](const MTPUpdates &result, const MTP::Response &response) {
}, [=](const MTP::Error &error, const MTP::Response &response) {
api->sendMessageFail(error, peer, randomId, newId);
});
api->finishForwarding(action);
return true;
}
void SendLocation(SendAction action, float64 lat, float64 lon) {
SendSimpleMedia(
action,
MTP_inputMediaGeoPoint(
MTP_inputGeoPoint(
MTP_flags(0),
MTP_double(lat),
MTP_double(lon),
MTPint()))); // accuracy_radius
}
void SendVenue(SendAction action, Data::InputVenue venue) {
SendSimpleMedia(
action,
MTP_inputMediaVenue(
MTP_inputGeoPoint(
MTP_flags(0),
MTP_double(venue.lat),
MTP_double(venue.lon),
MTPint()), // accuracy_radius
MTP_string(venue.title),
MTP_string(venue.address),
MTP_string(venue.provider),
MTP_string(venue.id),
MTP_string(venue.venueType)));
}
void FillMessagePostFlags(
const SendAction &action,
not_null<PeerData*> peer,
MessageFlags &flags) {
InnerFillMessagePostFlags(action.options, peer, flags);
}
void SendConfirmedFile(
not_null<Main::Session*> session,
const std::shared_ptr<FilePrepareResult> &file) {
const auto isEditing = (file->type != SendMediaType::Audio)
&& (file->type != SendMediaType::Round)
&& (file->to.replaceMediaOf != 0);
const auto newId = FullMsgId(
file->to.peer,
(isEditing
? file->to.replaceMediaOf
: session->data().nextLocalMessageId()));
const auto groupId = file->album ? file->album->groupId : uint64(0);
if (file->album) {
const auto proj = [](const SendingAlbum::Item &item) {
return item.taskId;
};
const auto it = ranges::find(file->album->items, file->taskId, proj);
Assert(it != file->album->items.end());
it->msgId = newId;
}
const auto itemToEdit = isEditing
? session->data().message(newId)
: nullptr;
const auto history = session->data().history(file->to.peer);
const auto peer = history->peer;
if (!isEditing) {
const auto histories = &session->data().histories();
file->to.replyTo.messageId = histories->convertTopicReplyToId(
history,
file->to.replyTo.messageId);
file->to.replyTo.topicRootId = histories->convertTopicReplyToId(
history,
file->to.replyTo.topicRootId);
}
session->uploader().upload(newId, file);
auto action = SendAction(history, file->to.options);
action.clearDraft = false;
action.replyTo = file->to.replyTo;
action.generateLocal = true;
action.replaceMediaOf = file->to.replaceMediaOf;
session->api().sendAction(action);
auto caption = TextWithEntities{
file->caption.text,
TextUtilities::ConvertTextTagsToEntities(file->caption.tags)
};
const auto prepareFlags = Ui::ItemTextOptions(
history,
session->user()).flags;
TextUtilities::PrepareForSending(caption, prepareFlags);
TextUtilities::Trim(caption);
auto flags = isEditing ? MessageFlags() : NewMessageFlags(peer);
if (file->to.replyTo) {
flags |= MessageFlag::HasReplyInfo;
}
FillMessagePostFlags(action, peer, flags);
if (file->to.options.scheduled) {
flags |= MessageFlag::IsOrWasScheduled;
// Scheduled messages have no 'edited' badge.
flags |= MessageFlag::HideEdited;
}
if (file->to.options.shortcutId) {
flags |= MessageFlag::ShortcutMessage;
// Shortcut messages have no 'edited' badge.
flags |= MessageFlag::HideEdited;
}
if (file->type == SendMediaType::Audio
|| file->type == SendMediaType::Round) {
if (!peer->isChannel() || peer->isMegagroup()) {
flags |= MessageFlag::MediaIsUnread;
}
}
if (file->to.options.invertCaption) {
flags |= MessageFlag::InvertMedia;
}
const auto media = MTPMessageMedia([&] {
if (file->type == SendMediaType::Photo) {
using Flag = MTPDmessageMediaPhoto::Flag;
return MTP_messageMediaPhoto(
MTP_flags(Flag::f_photo
| (file->spoiler ? Flag::f_spoiler : Flag())),
file->photo,
MTPint());
} else if (file->type == SendMediaType::File) {
using Flag = MTPDmessageMediaDocument::Flag;
return MTP_messageMediaDocument(
MTP_flags(Flag::f_document
| (file->spoiler ? Flag::f_spoiler : Flag())
| (file->videoCover ? Flag::f_video_cover : Flag())),
file->document,
MTPVector<MTPDocument>(), // alt_documents
file->videoCover ? file->videoCover->photo : MTPPhoto(),
MTPint(), // video_timestamp
MTPint());
} else if (file->type == SendMediaType::Audio) {
const auto ttlSeconds = file->to.options.ttlSeconds;
using Flag = MTPDmessageMediaDocument::Flag;
return MTP_messageMediaDocument(
MTP_flags(Flag::f_document
| Flag::f_voice
| (ttlSeconds ? Flag::f_ttl_seconds : Flag())
| (file->videoCover ? Flag::f_video_cover : Flag())),
file->document,
MTPVector<MTPDocument>(), // alt_documents
file->videoCover ? file->videoCover->photo : MTPPhoto(),
MTPint(), // video_timestamp
MTP_int(ttlSeconds));
} else if (file->type == SendMediaType::Round) {
using Flag = MTPDmessageMediaDocument::Flag;
const auto ttlSeconds = file->to.options.ttlSeconds;
return MTP_messageMediaDocument(
MTP_flags(Flag::f_document
| Flag::f_round
| (ttlSeconds ? Flag::f_ttl_seconds : Flag())
| (file->spoiler ? Flag::f_spoiler : Flag())),
file->document,
MTPVector<MTPDocument>(), // alt_documents
MTPPhoto(), // video_cover
MTPint(), // video_timestamp
MTP_int(ttlSeconds));
} else {
Unexpected("Type in sendFilesConfirmed.");
}
}());
if (itemToEdit) {
auto edition = HistoryMessageEdition();
edition.isEditHide = (flags & MessageFlag::HideEdited);
edition.editDate = 0;
edition.ttl = 0;
edition.mtpMedia = &media;
edition.textWithEntities = caption;
edition.invertMedia = file->to.options.invertCaption;
edition.useSameViews = true;
edition.useSameForwards = true;
edition.useSameMarkup = true;
edition.useSameReplies = true;
edition.useSameReactions = true;
edition.useSameSuggest = true;
edition.savePreviousMedia = true;
itemToEdit->applyEdition(std::move(edition));
} else {
history->addNewLocalMessage({
.id = newId.msg,
.flags = flags,
.from = NewMessageFromId(action),
.replyTo = file->to.replyTo,
.date = NewMessageDate(file->to.options),
.shortcutId = file->to.options.shortcutId,
.starsPaid = std::min(
history->peer->starsPerMessageChecked(),
file->to.options.starsApproved),
.postAuthor = NewMessagePostAuthor(action),
.groupedId = groupId,
.effectId = file->to.options.effectId,
.suggest = HistoryMessageSuggestInfo(file->to.options),
}, caption, media);
}
if (isEditing) {
return;
}
session->data().sendHistoryChangeNotifications();
if (!itemToEdit) {
session->changes().historyUpdated(
history,
(action.options.scheduled
? Data::HistoryUpdate::Flag::ScheduledSent
: Data::HistoryUpdate::Flag::MessageSent));
}
}
} // namespace Api

View File

@@ -0,0 +1,56 @@
/*
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
*/
#pragma once
class History;
class PhotoData;
class DocumentData;
struct FilePrepareResult;
namespace Data {
struct InputVenue;
} // namespace Data
namespace Main {
class Session;
} // namespace Main
namespace Api {
struct MessageToSend;
struct SendAction;
void SendExistingDocument(
MessageToSend &&message,
not_null<DocumentData*> document,
std::optional<MsgId> localMessageId = std::nullopt);
void SendExistingPhoto(
MessageToSend &&message,
not_null<PhotoData*> photo,
std::optional<MsgId> localMessageId = std::nullopt);
bool SendDice(MessageToSend &message);
// We can't create Data::LocationPoint() and use it
// for a local sending message, because we can't request
// map thumbnail in messages history without access hash.
void SendLocation(SendAction action, float64 lat, float64 lon);
void SendVenue(SendAction action, Data::InputVenue venue);
void FillMessagePostFlags(
const SendAction &action,
not_null<PeerData*> peer,
MessageFlags &flags);
void SendConfirmedFile(
not_null<Main::Session*> session,
const std::shared_ptr<FilePrepareResult> &file);
} // namespace Api

View File

@@ -0,0 +1,121 @@
/*
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 "api/api_sensitive_content.h"
#include "apiwrap.h"
#include "main/main_session.h"
#include "main/main_app_config.h"
namespace Api {
namespace {
constexpr auto kRefreshAppConfigTimeout = crl::time(1);
} // namespace
SensitiveContent::SensitiveContent(not_null<ApiWrap*> api)
: _session(&api->session())
, _api(&api->instance())
, _appConfigReloadTimer([=] { _session->appConfig().refresh(); }) {
}
void SensitiveContent::preload() {
if (!_loaded && !_loadRequestId) {
reload();
}
}
void SensitiveContent::reload(bool force) {
if (_loadRequestId) {
if (force) {
_loadPending = true;
}
return;
}
_loadRequestId = _api.request(MTPaccount_GetContentSettings(
)).done([=](const MTPaccount_ContentSettings &result) {
_loadRequestId = 0;
const auto &data = result.data();
const auto enabled = data.is_sensitive_enabled();
const auto canChange = data.is_sensitive_can_change();
const auto changed = (_enabled.current() != enabled)
|| (_canChange.current() != canChange);
if (changed) {
_enabled = enabled;
_canChange = canChange;
}
if (!_loaded) {
_loaded = true;
_loadedChanged.fire({});
}
if (base::take(_appConfigReloadForce) || changed) {
_appConfigReloadTimer.callOnce(kRefreshAppConfigTimeout);
}
if (base::take(_loadPending)) {
reload();
}
}).fail([=] {
_loadRequestId = 0;
}).send();
}
bool SensitiveContent::loaded() const {
return _loaded;
}
rpl::producer<bool> SensitiveContent::loadedValue() const {
if (_loaded) {
return rpl::single(true);
}
return rpl::single(false) | rpl::then(
_loadedChanged.events() | rpl::map_to(true)
);
}
bool SensitiveContent::enabledCurrent() const {
return _enabled.current();
}
rpl::producer<bool> SensitiveContent::enabled() const {
return _enabled.value();
}
bool SensitiveContent::canChangeCurrent() const {
return _canChange.current();
}
rpl::producer<bool> SensitiveContent::canChange() const {
return _canChange.value();
}
void SensitiveContent::update(bool enabled) {
if (!_canChange.current()) {
return;
}
using Flag = MTPaccount_SetContentSettings::Flag;
_api.request(_saveRequestId).cancel();
if (const auto load = base::take(_loadRequestId)) {
_api.request(load).cancel();
_loadPending = true;
}
const auto finish = [=] {
_saveRequestId = 0;
if (base::take(_loadPending)) {
_appConfigReloadForce = true;
reload(true);
} else {
_appConfigReloadTimer.callOnce(kRefreshAppConfigTimeout);
}
};
_saveRequestId = _api.request(MTPaccount_SetContentSettings(
MTP_flags(enabled ? Flag::f_sensitive_enabled : Flag(0))
)).done(finish).fail(finish).send();
_enabled = enabled;
}
} // namespace Api

View File

@@ -0,0 +1,51 @@
/*
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
*/
#pragma once
#include "mtproto/sender.h"
#include "base/timer.h"
class ApiWrap;
namespace Main {
class Session;
} // namespace Main
namespace Api {
class SensitiveContent final {
public:
explicit SensitiveContent(not_null<ApiWrap*> api);
void preload();
void reload(bool force = false);
void update(bool enabled);
[[nodiscard]] bool loaded() const;
[[nodiscard]] rpl::producer<bool> loadedValue() const;
[[nodiscard]] bool enabledCurrent() const;
[[nodiscard]] rpl::producer<bool> enabled() const;
[[nodiscard]] bool canChangeCurrent() const;
[[nodiscard]] rpl::producer<bool> canChange() const;
private:
const not_null<Main::Session*> _session;
rpl::event_stream<> _loadedChanged;
MTP::Sender _api;
mtpRequestId _loadRequestId = 0;
mtpRequestId _saveRequestId = 0;
rpl::variable<bool> _enabled = false;
rpl::variable<bool> _canChange = false;
base::Timer _appConfigReloadTimer;
bool _appConfigReloadForce = false;
bool _loadPending = false;
bool _loaded = false;
};
} // namespace Api

View File

@@ -0,0 +1,237 @@
/*
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 "api/api_single_message_search.h"
#include "main/main_session.h"
#include "data/data_session.h"
#include "data/data_channel.h"
#include "data/data_search_controller.h"
#include "core/local_url_handlers.h"
#include "history/history_item.h"
#include "base/qthelp_url.h"
#include "apiwrap.h"
namespace Api {
namespace {
using Key = details::SingleMessageSearchKey;
Key ExtractKey(const QString &query) {
const auto trimmed = query.trimmed();
const auto local = Core::TryConvertUrlToLocal(trimmed);
const auto check = local.isEmpty() ? trimmed : local;
const auto parse = [&] {
const auto delimeter = check.indexOf('?');
return (delimeter > 0)
? qthelp::url_parse_params(
check.mid(delimeter + 1),
qthelp::UrlParamNameTransform::ToLower)
: QMap<QString, QString>();
};
if (check.startsWith(u"tg://privatepost"_q, Qt::CaseInsensitive)) {
const auto params = parse();
const auto channel = params.value("channel");
const auto post = params.value("post").toInt();
return (channel.toULongLong() && post) ? Key{ channel, post } : Key();
} else if (check.startsWith(u"tg://resolve"_q, Qt::CaseInsensitive)) {
const auto params = parse();
const auto domain = params.value("domain");
const auto post = params.value("post").toInt();
return (!domain.isEmpty() && post) ? Key{ domain, post } : Key();
}
return Key();
}
} // namespace
SingleMessageSearch::SingleMessageSearch(not_null<Main::Session*> session)
: _session(session) {
}
SingleMessageSearch::~SingleMessageSearch() {
clear();
}
void SingleMessageSearch::clear() {
_cache.clear();
_requestKey = Key();
_session->api().request(base::take(_requestId)).cancel();
}
std::optional<HistoryItem*> SingleMessageSearch::lookup(
const QString &query,
Fn<void()> ready) {
const auto key = ExtractKey(query);
if (!key) {
return nullptr;
}
const auto i = _cache.find(key);
if (i != end(_cache)) {
return _session->data().message(i->second);
}
if (!(_requestKey == key)) {
_session->api().request(base::take(_requestId)).cancel();
_requestKey = key;
}
return performLookup(ready);
}
std::optional<HistoryItem*> SingleMessageSearch::performLookupByChannel(
not_null<ChannelData*> channel,
Fn<void()> ready) {
Expects(!_requestKey.empty());
const auto postId = _requestKey.postId;
if (const auto item = _session->data().message(channel->id, postId)) {
_cache.emplace(_requestKey, item->fullId());
return item;
} else if (!ready) {
return nullptr;
}
const auto fail = [=] {
_cache.emplace(_requestKey, FullMsgId());
ready();
};
_requestId = _session->api().request(MTPchannels_GetMessages(
channel->inputChannel(),
MTP_vector<MTPInputMessage>(1, MTP_inputMessageID(MTP_int(postId)))
)).done([=](const MTPmessages_Messages &result) {
const auto received = Api::ParseSearchResult(
channel,
Storage::SharedMediaType::kCount,
postId,
Data::LoadDirection::Around,
result);
if (!received.messageIds.empty()
&& received.messageIds.front() == postId) {
_cache.emplace(
_requestKey,
FullMsgId(channel->id, postId));
ready();
} else {
fail();
}
}).fail([=] {
fail();
}).send();
return std::nullopt;
}
std::optional<HistoryItem*> SingleMessageSearch::performLookupById(
ChannelId channelId,
Fn<void()> ready) {
Expects(!_requestKey.empty());
if (const auto channel = _session->data().channelLoaded(channelId)) {
return performLookupByChannel(channel, ready);
} else if (!ready) {
return nullptr;
}
const auto fail = [=] {
_cache.emplace(_requestKey, FullMsgId());
ready();
};
_requestId = _session->api().request(MTPchannels_GetChannels(
MTP_vector<MTPInputChannel>(
1,
MTP_inputChannel(MTP_long(channelId.bare), MTP_long(0)))
)).done([=](const MTPmessages_Chats &result) {
result.match([&](const auto &data) {
const auto peer = _session->data().processChats(data.vchats());
if (peer && peer->id == peerFromChannel(channelId)) {
if (performLookupByChannel(peer->asChannel(), ready)) {
ready();
}
} else {
fail();
}
});
}).fail([=] {
fail();
}).send();
return std::nullopt;
}
std::optional<HistoryItem*> SingleMessageSearch::performLookupByUsername(
const QString &username,
Fn<void()> ready) {
Expects(!_requestKey.empty());
if (const auto peer = _session->data().peerByUsername(username)) {
if (const auto channel = peer->asChannel()) {
return performLookupByChannel(channel, ready);
}
_cache.emplace(_requestKey, FullMsgId());
return nullptr;
} else if (!ready) {
return nullptr;
}
const auto fail = [=] {
_cache.emplace(_requestKey, FullMsgId());
ready();
};
_requestId = _session->api().request(MTPcontacts_ResolveUsername(
MTP_flags(0),
MTP_string(username),
MTP_string()
)).done([=](const MTPcontacts_ResolvedPeer &result) {
result.match([&](const MTPDcontacts_resolvedPeer &data) {
_session->data().processUsers(data.vusers());
_session->data().processChats(data.vchats());
const auto peerId = peerFromMTP(data.vpeer());
const auto peer = peerId
? _session->data().peerLoaded(peerId)
: nullptr;
if (const auto channel = peer ? peer->asChannel() : nullptr) {
if (performLookupByChannel(channel, ready)) {
ready();
}
} else {
fail();
}
});
}).fail([=] {
fail();
}).send();
return std::nullopt;
}
std::optional<HistoryItem*> SingleMessageSearch::performLookup(
Fn<void()> ready) {
Expects(!_requestKey.empty());
if (!_requestKey.domainOrId[0].isDigit()) {
return performLookupByUsername(_requestKey.domainOrId, ready);
}
const auto channelId = ChannelId(_requestKey.domainOrId.toULongLong());
return performLookupById(channelId, ready);
}
QString ConvertPeerSearchQuery(const QString &query) {
const auto trimmed = query.trimmed();
const auto local = Core::TryConvertUrlToLocal(trimmed);
const auto check = local.isEmpty() ? trimmed : local;
if (!check.startsWith(u"tg://resolve"_q, Qt::CaseInsensitive)) {
return query;
}
const auto delimeter = check.indexOf('?');
const auto params = (delimeter > 0)
? qthelp::url_parse_params(
check.mid(delimeter + 1),
qthelp::UrlParamNameTransform::ToLower)
: QMap<QString, QString>();
return params.value("domain", query);
}
} // namespace Api

View File

@@ -0,0 +1,76 @@
/*
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
*/
#pragma once
namespace Main {
class Session;
} // namespace Main
namespace Api {
namespace details {
struct SingleMessageSearchKey {
QString domainOrId;
MsgId postId = 0;
[[nodiscard]] bool empty() const {
return domainOrId.isEmpty() || !postId;
}
[[nodiscard]] explicit operator bool() const {
return !empty();
}
[[nodiscard]] bool operator<(const SingleMessageSearchKey &other) const {
return std::tie(domainOrId, postId)
< std::tie(other.domainOrId, other.postId);
}
[[nodiscard]] bool operator==(
const SingleMessageSearchKey &other) const {
return std::tie(domainOrId, postId)
== std::tie(other.domainOrId, other.postId);
}
};
} // namespace details
class SingleMessageSearch {
public:
explicit SingleMessageSearch(not_null<Main::Session*> session);
~SingleMessageSearch();
void clear();
// If 'ready' callback is empty, the result must not be 'nullopt'.
[[nodiscard]] std::optional<HistoryItem*> lookup(
const QString &query,
Fn<void()> ready = nullptr);
private:
using Key = details::SingleMessageSearchKey;
[[nodiscard]] std::optional<HistoryItem*> performLookup(
Fn<void()> ready);
[[nodiscard]] std::optional<HistoryItem*> performLookupById(
ChannelId channelId,
Fn<void()> ready);
[[nodiscard]] std::optional<HistoryItem*> performLookupByUsername(
const QString &username,
Fn<void()> ready);
[[nodiscard]] std::optional<HistoryItem*> performLookupByChannel(
not_null<ChannelData*> channel,
Fn<void()> ready);
const not_null<Main::Session*> _session;
std::map<Key, FullMsgId> _cache;
mtpRequestId _requestId = 0;
Key _requestKey;
};
[[nodiscard]] QString ConvertPeerSearchQuery(const QString &query);
} // namespace Api

View File

@@ -0,0 +1,794 @@
/*
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 "api/api_statistics.h"
#include "api/api_credits_history_entry.h"
#include "api/api_statistics_data_deserialize.h"
#include "apiwrap.h"
#include "base/unixtime.h"
#include "data/data_channel.h"
#include "data/data_session.h"
#include "data/data_stories.h"
#include "data/data_story.h"
#include "data/data_user.h"
#include "history/history.h"
#include "main/main_session.h"
namespace Api {
namespace {
[[nodiscard]] Data::StatisticalValue StatisticalValueFromTL(
const MTPStatsAbsValueAndPrev &tl) {
const auto current = tl.data().vcurrent().v;
const auto previous = tl.data().vprevious().v;
return Data::StatisticalValue{
.value = current,
.previousValue = previous,
.growthRatePercentage = previous
? std::abs((current - previous) / float64(previous) * 100.)
: 0,
};
}
[[nodiscard]] Data::ChannelStatistics ChannelStatisticsFromTL(
const MTPDstats_broadcastStats &data) {
const auto &tlUnmuted = data.venabled_notifications().data();
const auto unmuted = (!tlUnmuted.vtotal().v)
? 0.
: std::clamp(
tlUnmuted.vpart().v / tlUnmuted.vtotal().v * 100.,
0.,
100.);
using Recent = MTPPostInteractionCounters;
auto recentMessages = ranges::views::all(
data.vrecent_posts_interactions().v
) | ranges::views::transform([&](const Recent &tl) {
return tl.match([&](const MTPDpostInteractionCountersStory &data) {
return Data::StatisticsMessageInteractionInfo{
.storyId = data.vstory_id().v,
.viewsCount = data.vviews().v,
.forwardsCount = data.vforwards().v,
.reactionsCount = data.vreactions().v,
};
}, [&](const MTPDpostInteractionCountersMessage &data) {
return Data::StatisticsMessageInteractionInfo{
.messageId = data.vmsg_id().v,
.viewsCount = data.vviews().v,
.forwardsCount = data.vforwards().v,
.reactionsCount = data.vreactions().v,
};
});
}) | ranges::to_vector;
return {
.startDate = data.vperiod().data().vmin_date().v,
.endDate = data.vperiod().data().vmax_date().v,
.memberCount = StatisticalValueFromTL(data.vfollowers()),
.meanViewCount = StatisticalValueFromTL(data.vviews_per_post()),
.meanShareCount = StatisticalValueFromTL(data.vshares_per_post()),
.meanReactionCount = StatisticalValueFromTL(
data.vreactions_per_post()),
.meanStoryViewCount = StatisticalValueFromTL(
data.vviews_per_story()),
.meanStoryShareCount = StatisticalValueFromTL(
data.vshares_per_story()),
.meanStoryReactionCount = StatisticalValueFromTL(
data.vreactions_per_story()),
.enabledNotificationsPercentage = unmuted,
.memberCountGraph = StatisticalGraphFromTL(
data.vgrowth_graph()),
.joinGraph = StatisticalGraphFromTL(
data.vfollowers_graph()),
.muteGraph = StatisticalGraphFromTL(
data.vmute_graph()),
.viewCountByHourGraph = StatisticalGraphFromTL(
data.vtop_hours_graph()),
.viewCountBySourceGraph = StatisticalGraphFromTL(
data.vviews_by_source_graph()),
.joinBySourceGraph = StatisticalGraphFromTL(
data.vnew_followers_by_source_graph()),
.languageGraph = StatisticalGraphFromTL(
data.vlanguages_graph()),
.messageInteractionGraph = StatisticalGraphFromTL(
data.vinteractions_graph()),
.instantViewInteractionGraph = StatisticalGraphFromTL(
data.viv_interactions_graph()),
.reactionsByEmotionGraph = StatisticalGraphFromTL(
data.vreactions_by_emotion_graph()),
.storyInteractionsGraph = StatisticalGraphFromTL(
data.vstory_interactions_graph()),
.storyReactionsByEmotionGraph = StatisticalGraphFromTL(
data.vstory_reactions_by_emotion_graph()),
.recentMessageInteractions = std::move(recentMessages),
};
}
[[nodiscard]] Data::SupergroupStatistics SupergroupStatisticsFromTL(
const MTPDstats_megagroupStats &data) {
using Senders = MTPStatsGroupTopPoster;
using Administrators = MTPStatsGroupTopAdmin;
using Inviters = MTPStatsGroupTopInviter;
auto topSenders = ranges::views::all(
data.vtop_posters().v
) | ranges::views::transform([&](const Senders &tl) {
return Data::StatisticsMessageSenderInfo{
.userId = UserId(tl.data().vuser_id().v),
.sentMessageCount = tl.data().vmessages().v,
.averageCharacterCount = tl.data().vavg_chars().v,
};
}) | ranges::to_vector;
auto topAdministrators = ranges::views::all(
data.vtop_admins().v
) | ranges::views::transform([&](const Administrators &tl) {
return Data::StatisticsAdministratorActionsInfo{
.userId = UserId(tl.data().vuser_id().v),
.deletedMessageCount = tl.data().vdeleted().v,
.bannedUserCount = tl.data().vkicked().v,
.restrictedUserCount = tl.data().vbanned().v,
};
}) | ranges::to_vector;
auto topInviters = ranges::views::all(
data.vtop_inviters().v
) | ranges::views::transform([&](const Inviters &tl) {
return Data::StatisticsInviterInfo{
.userId = UserId(tl.data().vuser_id().v),
.addedMemberCount = tl.data().vinvitations().v,
};
}) | ranges::to_vector;
return {
.startDate = data.vperiod().data().vmin_date().v,
.endDate = data.vperiod().data().vmax_date().v,
.memberCount = StatisticalValueFromTL(data.vmembers()),
.messageCount = StatisticalValueFromTL(data.vmessages()),
.viewerCount = StatisticalValueFromTL(data.vviewers()),
.senderCount = StatisticalValueFromTL(data.vposters()),
.memberCountGraph = StatisticalGraphFromTL(
data.vgrowth_graph()),
.joinGraph = StatisticalGraphFromTL(
data.vmembers_graph()),
.joinBySourceGraph = StatisticalGraphFromTL(
data.vnew_members_by_source_graph()),
.languageGraph = StatisticalGraphFromTL(
data.vlanguages_graph()),
.messageContentGraph = StatisticalGraphFromTL(
data.vmessages_graph()),
.actionGraph = StatisticalGraphFromTL(
data.vactions_graph()),
.dayGraph = StatisticalGraphFromTL(
data.vtop_hours_graph()),
.weekGraph = StatisticalGraphFromTL(
data.vweekdays_graph()),
.topSenders = std::move(topSenders),
.topAdministrators = std::move(topAdministrators),
.topInviters = std::move(topInviters),
};
}
} // namespace
Statistics::Statistics(not_null<ChannelData*> channel)
: StatisticsRequestSender(channel) {
}
rpl::producer<rpl::no_value, QString> Statistics::request() {
return [=](auto consumer) {
auto lifetime = rpl::lifetime();
if (!channel()->isMegagroup()) {
makeRequest(MTPstats_GetBroadcastStats(
MTP_flags(MTPstats_GetBroadcastStats::Flags(0)),
channel()->inputChannel()
)).done([=](const MTPstats_BroadcastStats &result) {
_channelStats = ChannelStatisticsFromTL(result.data());
consumer.put_done();
}).fail([=](const MTP::Error &error) {
consumer.put_error_copy(error.type());
}).send();
} else {
makeRequest(MTPstats_GetMegagroupStats(
MTP_flags(MTPstats_GetMegagroupStats::Flags(0)),
channel()->inputChannel()
)).done([=](const MTPstats_MegagroupStats &result) {
const auto &data = result.data();
_supergroupStats = SupergroupStatisticsFromTL(data);
channel()->owner().processUsers(data.vusers());
consumer.put_done();
}).fail([=](const MTP::Error &error) {
consumer.put_error_copy(error.type());
}).send();
}
return lifetime;
};
}
Statistics::GraphResult Statistics::requestZoom(
const QString &token,
float64 x) {
return [=](auto consumer) {
auto lifetime = rpl::lifetime();
const auto wasEmpty = _zoomDeque.empty();
_zoomDeque.push_back([=] {
makeRequest(MTPstats_LoadAsyncGraph(
MTP_flags(x
? MTPstats_LoadAsyncGraph::Flag::f_x
: MTPstats_LoadAsyncGraph::Flag(0)),
MTP_string(token),
MTP_long(x)
)).done([=](const MTPStatsGraph &result) {
consumer.put_next(StatisticalGraphFromTL(result));
consumer.put_done();
if (!_zoomDeque.empty()) {
_zoomDeque.pop_front();
if (!_zoomDeque.empty()) {
_zoomDeque.front()();
}
}
}).fail([=](const MTP::Error &error) {
consumer.put_error_copy(error.type());
}).send();
});
if (wasEmpty) {
_zoomDeque.front()();
}
return lifetime;
};
}
Data::ChannelStatistics Statistics::channelStats() const {
return _channelStats;
}
Data::SupergroupStatistics Statistics::supergroupStats() const {
return _supergroupStats;
}
PublicForwards::PublicForwards(
not_null<ChannelData*> channel,
Data::RecentPostId fullId)
: StatisticsRequestSender(channel)
, _fullId(fullId) {
}
void PublicForwards::request(
const Data::PublicForwardsSlice::OffsetToken &token,
Fn<void(Data::PublicForwardsSlice)> done) {
if (_requestId) {
return;
}
const auto channel = StatisticsRequestSender::channel();
const auto processResult = [=](const MTPstats_PublicForwards &tl) {
using Messages = QVector<Data::RecentPostId>;
_requestId = 0;
const auto &data = tl.data();
auto &owner = channel->owner();
owner.processUsers(data.vusers());
owner.processChats(data.vchats());
const auto nextToken = data.vnext_offset()
? qs(*data.vnext_offset())
: Data::PublicForwardsSlice::OffsetToken();
const auto fullCount = data.vcount().v;
auto recentList = Messages(data.vforwards().v.size());
for (const auto &tlForward : data.vforwards().v) {
tlForward.match([&](const MTPDpublicForwardMessage &data) {
const auto &message = data.vmessage();
const auto msgId = IdFromMessage(message);
const auto peerId = PeerFromMessage(message);
const auto lastDate = DateFromMessage(message);
if (owner.peerLoaded(peerId)) {
if (!lastDate) {
return;
}
owner.addNewMessage(
message,
MessageFlags(),
NewMessageType::Existing);
recentList.push_back({ .messageId = { peerId, msgId } });
}
}, [&](const MTPDpublicForwardStory &data) {
const auto story = owner.stories().applySingle(
peerFromMTP(data.vpeer()),
data.vstory());
if (story) {
recentList.push_back({ .storyId = story->fullId() });
}
});
}
const auto allLoaded = nextToken.isEmpty() || (nextToken == token);
_lastTotal = std::max(_lastTotal, fullCount);
done({
.list = std::move(recentList),
.total = _lastTotal,
.allLoaded = allLoaded,
.token = nextToken,
});
};
const auto processFail = [=] {
_requestId = 0;
done({});
};
constexpr auto kLimit = tl::make_int(100);
if (_fullId.messageId) {
_requestId = makeRequest(MTPstats_GetMessagePublicForwards(
channel->inputChannel(),
MTP_int(_fullId.messageId.msg),
MTP_string(token),
kLimit
)).done(processResult).fail(processFail).send();
} else if (_fullId.storyId) {
_requestId = makeRequest(MTPstats_GetStoryPublicForwards(
channel->input(),
MTP_int(_fullId.storyId.story),
MTP_string(token),
kLimit
)).done(processResult).fail(processFail).send();
}
}
MessageStatistics::MessageStatistics(
not_null<ChannelData*> channel,
FullMsgId fullId)
: StatisticsRequestSender(channel)
, _publicForwards(channel, { .messageId = fullId })
, _fullId(fullId) {
}
MessageStatistics::MessageStatistics(
not_null<ChannelData*> channel,
FullStoryId storyId)
: StatisticsRequestSender(channel)
, _publicForwards(channel, { .storyId = storyId })
, _storyId(storyId) {
}
Data::PublicForwardsSlice MessageStatistics::firstSlice() const {
return _firstSlice;
}
void MessageStatistics::request(Fn<void(Data::MessageStatistics)> done) {
if (channel()->isMegagroup() && !_storyId) {
return;
}
const auto requestFirstPublicForwards = [=](
const Data::StatisticalGraph &messageGraph,
const Data::StatisticalGraph &reactionsGraph,
const Data::StatisticsMessageInteractionInfo &info) {
const auto callback = [=](Data::PublicForwardsSlice slice) {
const auto total = slice.total;
_firstSlice = std::move(slice);
done({
.messageInteractionGraph = messageGraph,
.reactionsByEmotionGraph = reactionsGraph,
.publicForwards = total,
.privateForwards = info.forwardsCount - total,
.views = info.viewsCount,
.reactions = info.reactionsCount,
});
};
_publicForwards.request({}, callback);
};
const auto requestPrivateForwards = [=](
const Data::StatisticalGraph &messageGraph,
const Data::StatisticalGraph &reactionsGraph) {
api().request(MTPchannels_GetMessages(
channel()->inputChannel(),
MTP_vector<MTPInputMessage>(
1,
MTP_inputMessageID(MTP_int(_fullId.msg))))
).done([=](const MTPmessages_Messages &result) {
const auto process = [&](const MTPVector<MTPMessage> &messages) {
const auto &message = messages.v.front();
return message.match([&](const MTPDmessage &data) {
auto reactionsCount = 0;
if (const auto tlReactions = data.vreactions()) {
const auto &tlCounts = tlReactions->data().vresults();
for (const auto &tlCount : tlCounts.v) {
reactionsCount += tlCount.data().vcount().v;
}
}
return Data::StatisticsMessageInteractionInfo{
.messageId = IdFromMessage(message),
.viewsCount = data.vviews()
? data.vviews()->v
: 0,
.forwardsCount = data.vforwards()
? data.vforwards()->v
: 0,
.reactionsCount = reactionsCount,
};
}, [](const MTPDmessageEmpty &) {
return Data::StatisticsMessageInteractionInfo();
}, [](const MTPDmessageService &) {
return Data::StatisticsMessageInteractionInfo();
});
};
auto info = result.match([&](const MTPDmessages_messages &data) {
return process(data.vmessages());
}, [&](const MTPDmessages_messagesSlice &data) {
return process(data.vmessages());
}, [&](const MTPDmessages_channelMessages &data) {
return process(data.vmessages());
}, [](const MTPDmessages_messagesNotModified &) {
return Data::StatisticsMessageInteractionInfo();
});
requestFirstPublicForwards(
messageGraph,
reactionsGraph,
std::move(info));
}).fail([=](const MTP::Error &error) {
requestFirstPublicForwards(messageGraph, reactionsGraph, {});
}).send();
};
const auto requestStoryPrivateForwards = [=](
const Data::StatisticalGraph &messageGraph,
const Data::StatisticalGraph &reactionsGraph) {
api().request(MTPstories_GetStoriesByID(
channel()->input(),
MTP_vector<MTPint>(1, MTP_int(_storyId.story)))
).done([=](const MTPstories_Stories &result) {
const auto &storyItem = result.data().vstories().v.front();
auto info = storyItem.match([&](const MTPDstoryItem &data) {
if (!data.vviews()) {
return Data::StatisticsMessageInteractionInfo();
}
const auto &tlViews = data.vviews()->data();
return Data::StatisticsMessageInteractionInfo{
.storyId = data.vid().v,
.viewsCount = tlViews.vviews_count().v,
.forwardsCount = tlViews.vforwards_count().value_or(0),
.reactionsCount = tlViews.vreactions_count().value_or(0),
};
}, [](const auto &) {
return Data::StatisticsMessageInteractionInfo();
});
requestFirstPublicForwards(
messageGraph,
reactionsGraph,
std::move(info));
}).fail([=](const MTP::Error &error) {
requestFirstPublicForwards(messageGraph, reactionsGraph, {});
}).send();
};
if (_storyId) {
makeRequest(MTPstats_GetStoryStats(
MTP_flags(MTPstats_GetStoryStats::Flags(0)),
channel()->input(),
MTP_int(_storyId.story)
)).done([=](const MTPstats_StoryStats &result) {
const auto &data = result.data();
requestStoryPrivateForwards(
StatisticalGraphFromTL(data.vviews_graph()),
StatisticalGraphFromTL(data.vreactions_by_emotion_graph()));
}).fail([=](const MTP::Error &error) {
requestStoryPrivateForwards({}, {});
}).send();
} else {
makeRequest(MTPstats_GetMessageStats(
MTP_flags(MTPstats_GetMessageStats::Flags(0)),
channel()->inputChannel(),
MTP_int(_fullId.msg.bare)
)).done([=](const MTPstats_MessageStats &result) {
const auto &data = result.data();
requestPrivateForwards(
StatisticalGraphFromTL(data.vviews_graph()),
StatisticalGraphFromTL(data.vreactions_by_emotion_graph()));
}).fail([=](const MTP::Error &error) {
requestPrivateForwards({}, {});
}).send();
}
}
Boosts::Boosts(not_null<PeerData*> peer)
: _peer(peer)
, _api(&peer->session().api().instance()) {
}
rpl::producer<rpl::no_value, QString> Boosts::request() {
return [=](auto consumer) {
auto lifetime = rpl::lifetime();
const auto channel = _peer->asChannel();
if (!channel) {
return lifetime;
}
_api.request(MTPpremium_GetBoostsStatus(
_peer->input()
)).done([=](const MTPpremium_BoostsStatus &result) {
const auto &data = result.data();
channel->updateLevelHint(data.vlevel().v);
const auto hasPremium = !!data.vpremium_audience();
const auto premiumMemberCount = hasPremium
? std::max(0, int(data.vpremium_audience()->data().vpart().v))
: 0;
const auto participantCount = hasPremium
? std::max(
int(data.vpremium_audience()->data().vtotal().v),
premiumMemberCount)
: 0;
const auto premiumMemberPercentage = (participantCount > 0)
? (100. * premiumMemberCount / participantCount)
: 0;
const auto slots = data.vmy_boost_slots();
_boostStatus.overview = Data::BoostsOverview{
.group = channel->isMegagroup(),
.mine = slots ? int(slots->v.size()) : 0,
.level = std::max(data.vlevel().v, 0),
.boostCount = std::max(
data.vboosts().v,
data.vcurrent_level_boosts().v),
.currentLevelBoostCount = data.vcurrent_level_boosts().v,
.nextLevelBoostCount = data.vnext_level_boosts()
? data.vnext_level_boosts()->v
: 0,
.premiumMemberCount = premiumMemberCount,
.premiumMemberPercentage = premiumMemberPercentage,
};
_boostStatus.link = qs(data.vboost_url());
if (data.vprepaid_giveaways()) {
_boostStatus.prepaidGiveaway = ranges::views::all(
data.vprepaid_giveaways()->v
) | ranges::views::transform([](const MTPPrepaidGiveaway &r) {
return r.match([&](const MTPDprepaidGiveaway &data) {
return Data::BoostPrepaidGiveaway{
.date = base::unixtime::parse(data.vdate().v),
.id = data.vid().v,
.months = data.vmonths().v,
.quantity = data.vquantity().v,
};
}, [&](const MTPDprepaidStarsGiveaway &data) {
return Data::BoostPrepaidGiveaway{
.date = base::unixtime::parse(data.vdate().v),
.id = data.vid().v,
.credits = data.vstars().v,
.quantity = data.vquantity().v,
.boosts = data.vboosts().v,
};
});
}) | ranges::to_vector;
}
using namespace Data;
requestBoosts({ .gifts = false }, [=](BoostsListSlice &&slice) {
_boostStatus.firstSliceBoosts = std::move(slice);
requestBoosts({ .gifts = true }, [=](BoostsListSlice &&s) {
_boostStatus.firstSliceGifts = std::move(s);
consumer.put_done();
});
});
}).fail([=](const MTP::Error &error) {
consumer.put_error_copy(error.type());
}).send();
return lifetime;
};
}
void Boosts::requestBoosts(
const Data::BoostsListSlice::OffsetToken &token,
Fn<void(Data::BoostsListSlice)> done) {
if (_requestId) {
return;
}
constexpr auto kTlFirstSlice = tl::make_int(kFirstSlice);
constexpr auto kTlLimit = tl::make_int(kLimit);
const auto gifts = token.gifts;
_requestId = _api.request(MTPpremium_GetBoostsList(
gifts
? MTP_flags(MTPpremium_GetBoostsList::Flag::f_gifts)
: MTP_flags(0),
_peer->input(),
MTP_string(token.next),
token.next.isEmpty() ? kTlFirstSlice : kTlLimit
)).done([=](const MTPpremium_BoostsList &result) {
_requestId = 0;
const auto &data = result.data();
_peer->owner().processUsers(data.vusers());
auto list = std::vector<Data::Boost>();
list.reserve(data.vboosts().v.size());
constexpr auto kMonthsDivider = int(30 * 86400);
for (const auto &boost : data.vboosts().v) {
const auto &data = boost.data();
const auto path = data.vused_gift_slug()
? (u"giftcode/"_q + qs(data.vused_gift_slug()->v))
: QString();
auto giftCodeLink = !path.isEmpty()
? Data::GiftCodeLink{
_peer->session().createInternalLink(path),
_peer->session().createInternalLinkFull(path),
qs(data.vused_gift_slug()->v),
}
: Data::GiftCodeLink();
list.push_back({
.id = qs(data.vid()),
.userId = UserId(data.vuser_id().value_or_empty()),
.giveawayMessage = data.vgiveaway_msg_id()
? FullMsgId{ _peer->id, data.vgiveaway_msg_id()->v }
: FullMsgId(),
.date = base::unixtime::parse(data.vdate().v),
.expiresAt = base::unixtime::parse(data.vexpires().v),
.expiresAfterMonths = ((data.vexpires().v - data.vdate().v)
/ kMonthsDivider),
.giftCodeLink = std::move(giftCodeLink),
.multiplier = data.vmultiplier().value_or_empty(),
.credits = data.vstars().value_or_empty(),
.isGift = data.is_gift(),
.isGiveaway = data.is_giveaway(),
.isUnclaimed = data.is_unclaimed(),
});
}
done(Data::BoostsListSlice{
.list = std::move(list),
.multipliedTotal = data.vcount().v,
.allLoaded = (data.vcount().v == data.vboosts().v.size()),
.token = Data::BoostsListSlice::OffsetToken{
.next = data.vnext_offset()
? qs(*data.vnext_offset())
: QString(),
.gifts = gifts,
},
});
}).fail([=] {
_requestId = 0;
}).send();
}
Data::BoostStatus Boosts::boostStatus() const {
return _boostStatus;
}
EarnStatistics::EarnStatistics(not_null<PeerData*> peer)
: StatisticsRequestSender(peer)
, _isUser(peer->isUser()) {
}
rpl::producer<rpl::no_value, QString> EarnStatistics::request() {
return [=](auto consumer) {
auto lifetime = rpl::lifetime();
api().request(MTPpayments_GetStarsRevenueStats(
MTP_flags(MTPpayments_getStarsRevenueStats::Flag::f_ton),
(_isUser ? user()->input() : channel()->input())
)).done([=](const MTPpayments_StarsRevenueStats &result) {
const auto &data = result.data();
const auto &balances = data.vstatus().data();
const auto amount = [](const auto &a) {
return CreditsAmountFromTL(a);
};
_data = Data::EarnStatistics{
.topHoursGraph = data.vtop_hours_graph()
? StatisticalGraphFromTL(*data.vtop_hours_graph())
: Data::StatisticalGraph(),
.revenueGraph = StatisticalGraphFromTL(data.vrevenue_graph()),
.currentBalance = amount(balances.vcurrent_balance()),
.availableBalance = amount(balances.vavailable_balance()),
.overallRevenue = amount(balances.voverall_revenue()),
.usdRate = data.vusd_rate().v,
};
requestHistory({}, [=](Data::EarnHistorySlice &&slice) {
_data.firstHistorySlice = std::move(slice);
if (!_isUser) {
api().request(
MTPchannels_GetFullChannel(channel()->inputChannel())
).done([=](const MTPmessages_ChatFull &result) {
result.data().vfull_chat().match([&](
const MTPDchannelFull &d) {
_data.switchedOff = d.is_restricted_sponsored();
}, [](const auto &) {
});
consumer.put_done();
}).fail([=](const MTP::Error &error) {
consumer.put_error_copy(error.type());
}).send();
} else {
consumer.put_done();
}
});
}).fail([=](const MTP::Error &error) {
consumer.put_error_copy(error.type());
}).send();
return lifetime;
};
}
void EarnStatistics::requestHistory(
const Data::EarnHistorySlice::OffsetToken &token,
Fn<void(Data::EarnHistorySlice)> done) {
if (_requestId) {
return;
}
constexpr auto kTlFirstSlice = tl::make_int(kFirstSlice);
constexpr auto kTlLimit = tl::make_int(kLimit);
_requestId = api().request(MTPpayments_GetStarsTransactions(
MTP_flags(MTPpayments_getStarsTransactions::Flag::f_ton),
MTP_string(), // Subscription ID.
(_isUser ? user()->input() : channel()->input()),
MTP_string(token),
token.isEmpty() ? kTlFirstSlice : kTlLimit
)).done([=](const MTPpayments_StarsStatus &result) {
_requestId = 0;
const auto nextToken = result.data().vnext_offset().value_or_empty();
const auto tlTransactions
= result.data().vhistory().value_or_empty();
const auto peer = _isUser ? (PeerData*)user() : (PeerData*)channel();
auto list = ranges::views::all(
tlTransactions
) | ranges::views::transform([=](const auto &d) {
return CreditsHistoryEntryFromTL(d, peer);
}) | ranges::to_vector;
done(Data::EarnHistorySlice{
.list = std::move(list),
.total = int(tlTransactions.size()),
// .total = result.data().vcount().v,
.allLoaded = nextToken.isEmpty(),
.token = Data::EarnHistorySlice::OffsetToken(nextToken),
});
}).fail([=] {
done({});
_requestId = 0;
}).send();
}
Data::EarnStatistics EarnStatistics::data() const {
return _data;
}
} // namespace Api

View File

@@ -0,0 +1,127 @@
/*
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
*/
#pragma once
#include "api/api_statistics_sender.h"
#include "data/data_boosts.h"
#include "data/data_channel_earn.h"
#include "data/data_statistics.h"
class ChannelData;
class PeerData;
namespace Api {
class Statistics final : public StatisticsRequestSender {
public:
explicit Statistics(not_null<ChannelData*> channel);
[[nodiscard]] rpl::producer<rpl::no_value, QString> request();
using GraphResult = rpl::producer<Data::StatisticalGraph, QString>;
[[nodiscard]] GraphResult requestZoom(
const QString &token,
float64 x);
[[nodiscard]] Data::ChannelStatistics channelStats() const;
[[nodiscard]] Data::SupergroupStatistics supergroupStats() const;
private:
Data::ChannelStatistics _channelStats;
Data::SupergroupStatistics _supergroupStats;
std::deque<Fn<void()>> _zoomDeque;
};
class PublicForwards final : public StatisticsRequestSender {
public:
PublicForwards(
not_null<ChannelData*> channel,
Data::RecentPostId fullId);
void request(
const Data::PublicForwardsSlice::OffsetToken &token,
Fn<void(Data::PublicForwardsSlice)> done);
private:
const Data::RecentPostId _fullId;
mtpRequestId _requestId = 0;
int _lastTotal = 0;
};
class MessageStatistics final : public StatisticsRequestSender {
public:
explicit MessageStatistics(
not_null<ChannelData*> channel,
FullMsgId fullId);
explicit MessageStatistics(
not_null<ChannelData*> channel,
FullStoryId storyId);
void request(Fn<void(Data::MessageStatistics)> done);
[[nodiscard]] Data::PublicForwardsSlice firstSlice() const;
private:
PublicForwards _publicForwards;
const FullMsgId _fullId;
const FullStoryId _storyId;
Data::PublicForwardsSlice _firstSlice;
mtpRequestId _requestId = 0;
};
class EarnStatistics final : public StatisticsRequestSender {
public:
explicit EarnStatistics(not_null<PeerData*> peer);
[[nodiscard]] rpl::producer<rpl::no_value, QString> request();
void requestHistory(
const Data::EarnHistorySlice::OffsetToken &token,
Fn<void(Data::EarnHistorySlice)> done);
[[nodiscard]] Data::EarnStatistics data() const;
static constexpr auto kFirstSlice = int(5);
static constexpr auto kLimit = int(10);
private:
const bool _isUser = false;
Data::EarnStatistics _data;
mtpRequestId _requestId = 0;
};
class Boosts final {
public:
explicit Boosts(not_null<PeerData*> peer);
[[nodiscard]] rpl::producer<rpl::no_value, QString> request();
void requestBoosts(
const Data::BoostsListSlice::OffsetToken &token,
Fn<void(Data::BoostsListSlice)> done);
[[nodiscard]] Data::BoostStatus boostStatus() const;
static constexpr auto kFirstSlice = int(10);
static constexpr auto kLimit = int(40);
private:
const not_null<PeerData*> _peer;
Data::BoostStatus _boostStatus;
MTP::Sender _api;
mtpRequestId _requestId = 0;
};
} // namespace Api

View File

@@ -0,0 +1,35 @@
/*
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 "api/api_statistics_data_deserialize.h"
#include "data/data_statistics_chart.h"
#include "statistics/statistics_data_deserialize.h"
namespace Api {
Data::StatisticalGraph StatisticalGraphFromTL(const MTPStatsGraph &tl) {
return tl.match([&](const MTPDstatsGraph &d) {
using namespace Statistic;
const auto zoomToken = d.vzoom_token().has_value()
? qs(*d.vzoom_token()).toUtf8()
: QByteArray();
return Data::StatisticalGraph{
StatisticalChartFromJSON(qs(d.vjson().data().vdata()).toUtf8()),
zoomToken,
};
}, [&](const MTPDstatsGraphAsync &data) {
return Data::StatisticalGraph{
.zoomToken = qs(data.vtoken()).toUtf8(),
};
}, [&](const MTPDstatsGraphError &data) {
return Data::StatisticalGraph{ .error = qs(data.verror()) };
});
}
} // namespace Api

View File

@@ -0,0 +1,19 @@
/*
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
*/
#pragma once
namespace Data {
struct StatisticalGraph;
} // namespace Data
namespace Api {
[[nodiscard]] Data::StatisticalGraph StatisticalGraphFromTL(
const MTPStatsGraph &tl);
} // namespace Api

View File

@@ -0,0 +1,86 @@
/*
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 "api/api_statistics_sender.h"
#include "apiwrap.h"
#include "data/data_peer.h"
#include "data/data_session.h"
#include "main/main_session.h"
namespace Api {
StatisticsRequestSender::StatisticsRequestSender(
not_null<PeerData*> peer)
: _peer(peer)
, _channel(peer->asChannel())
, _user(peer->asUser())
, _api(&_peer->session().api().instance())
, _timer([=] { checkRequests(); }) {
}
MTP::Sender &StatisticsRequestSender::api() {
return _api;
}
not_null<ChannelData*> StatisticsRequestSender::channel() {
Expects(_channel);
return _channel;
}
not_null<UserData*> StatisticsRequestSender::user() {
Expects(_user);
return _user;
}
void StatisticsRequestSender::checkRequests() {
for (auto i = begin(_requests); i != end(_requests);) {
for (auto j = begin(i->second); j != end(i->second);) {
if (_api.pending(*j)) {
++j;
} else {
_peer->session().api().unregisterStatsRequest(
i->first,
*j);
j = i->second.erase(j);
}
}
if (i->second.empty()) {
i = _requests.erase(i);
} else {
++i;
}
}
if (_requests.empty()) {
_timer.cancel();
}
}
auto StatisticsRequestSender::ensureRequestIsRegistered()
-> StatisticsRequestSender::Registered {
const auto id = _api.allocateRequestId();
const auto dcId = _peer->owner().statsDcId(_peer);
if (dcId) {
_peer->session().api().registerStatsRequest(dcId, id);
_requests[dcId].emplace(id);
if (!_timer.isActive()) {
constexpr auto kCheckRequestsTimer = 10 * crl::time(1000);
_timer.callEach(kCheckRequestsTimer);
}
}
return StatisticsRequestSender::Registered{ id, dcId };
}
StatisticsRequestSender::~StatisticsRequestSender() {
for (const auto &[dcId, ids] : _requests) {
for (const auto id : ids) {
_peer->session().api().unregisterStatsRequest(dcId, id);
}
}
}
} // namespace Api

View File

@@ -0,0 +1,58 @@
/*
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
*/
#pragma once
#include "base/timer.h"
#include "mtproto/sender.h"
class ChannelData;
class PeerData;
class UserData;
namespace Api {
class StatisticsRequestSender {
protected:
explicit StatisticsRequestSender(not_null<PeerData*> peer);
~StatisticsRequestSender();
template <
typename Request,
typename = std::enable_if_t<!std::is_reference_v<Request>>,
typename = typename Request::Unboxed>
[[nodiscard]] auto makeRequest(Request &&request) {
const auto [id, dcId] = ensureRequestIsRegistered();
return std::move(_api.request(
std::forward<Request>(request)
).toDC(
dcId ? MTP::ShiftDcId(dcId, MTP::kStatsDcShift) : 0
).overrideId(id));
}
[[nodiscard]] MTP::Sender &api();
[[nodiscard]] not_null<ChannelData*> channel();
[[nodiscard]] not_null<UserData*> user();
private:
struct Registered final {
mtpRequestId id;
MTP::DcId dcId;
};
[[nodiscard]] Registered ensureRequestIsRegistered();
void checkRequests();
const not_null<PeerData*> _peer;
ChannelData * const _channel;
UserData * const _user;
MTP::Sender _api;
base::Timer _timer;
base::flat_map<MTP::DcId, base::flat_set<mtpRequestId>> _requests;
};
} // namespace Api

View File

@@ -0,0 +1,660 @@
/*
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 "api/api_suggest_post.h"
#include "apiwrap.h"
#include "base/unixtime.h"
#include "boxes/transfer_gift_box.h"
#include "chat_helpers/message_field.h"
#include "core/click_handler_types.h"
#include "data/components/credits.h"
#include "data/data_changes.h"
#include "data/data_channel.h"
#include "data/data_session.h"
#include "data/data_saved_sublist.h"
#include "history/view/controls/history_view_suggest_options.h"
#include "history/history.h"
#include "history/history_item.h"
#include "history/history_item_components.h"
#include "history/history_item_helpers.h"
#include "lang/lang_keys.h"
#include "main/main_session.h"
#include "mainwindow.h"
#include "settings/settings_credits_graphics.h"
#include "ui/boxes/choose_date_time.h"
#include "ui/layers/generic_box.h"
#include "ui/boxes/confirm_box.h"
#include "ui/text/text_utilities.h"
#include "ui/widgets/fields/input_field.h"
#include "ui/widgets/popup_menu.h"
#include "window/window_session_controller.h"
#include "styles/style_chat.h"
#include "styles/style_layers.h"
#include "styles/style_menu_icons.h"
namespace Api {
namespace {
void SendApproval(
std::shared_ptr<Main::SessionShow> show,
not_null<HistoryItem*> item,
TimeId scheduleDate = 0) {
using Flag = MTPmessages_ToggleSuggestedPostApproval::Flag;
const auto suggestion = item->Get<HistoryMessageSuggestion>();
if (!suggestion
|| suggestion->accepted
|| suggestion->rejected
|| suggestion->requestId) {
return;
}
const auto id = item->fullId();
const auto session = &show->session();
const auto finish = [=] {
if (const auto item = session->data().message(id)) {
const auto suggestion = item->Get<HistoryMessageSuggestion>();
if (suggestion) {
suggestion->requestId = 0;
}
}
};
suggestion->requestId = session->api().request(
MTPmessages_ToggleSuggestedPostApproval(
MTP_flags(scheduleDate ? Flag::f_schedule_date : Flag()),
item->history()->peer->input(),
MTP_int(item->id.bare),
MTP_int(scheduleDate),
MTPstring()) // reject_comment
).done([=](const MTPUpdates &result) {
session->api().applyUpdates(result);
finish();
}).fail([=](const MTP::Error &error) {
show->showToast(error.type());
finish();
}).send();
}
void ConfirmApproval(
std::shared_ptr<Main::SessionShow> show,
not_null<HistoryItem*> item,
TimeId scheduleDate = 0,
Fn<void()> accepted = nullptr) {
const auto suggestion = item->Get<HistoryMessageSuggestion>();
if (!suggestion
|| suggestion->accepted
|| suggestion->rejected
|| suggestion->requestId) {
return;
}
const auto id = item->fullId();
const auto price = suggestion->price;
const auto admin = item->history()->amMonoforumAdmin();
if (!admin && !price.empty()) {
const auto credits = &item->history()->session().credits();
if (price.ton()) {
if (!credits->tonLoaded()) {
credits->tonLoad();
return;
} else if (price > credits->tonBalance()) {
const auto peer = item->history()->peer;
show->show(
Box(HistoryView::InsufficientTonBox, peer, price));
return;
}
} else {
if (!credits->loaded()) {
credits->load();
return;
} else if (price > credits->balance()) {
using namespace Settings;
const auto peer = item->history()->peer;
const auto broadcast = peer->monoforumBroadcast();
const auto broadcastId = (broadcast ? broadcast : peer)->id;
const auto done = [=](SmallBalanceResult result) {
if (result == SmallBalanceResult::Success
|| result == SmallBalanceResult::Already) {
const auto item = peer->owner().message(id);
if (item) {
ConfirmApproval(
show,
item,
scheduleDate,
accepted);
}
}
};
MaybeRequestBalanceIncrease(
show,
int(base::SafeRound(price.value())),
SmallBalanceForSuggest{ broadcastId },
done);
return;
}
}
}
const auto peer = item->history()->peer;
const auto session = &peer->session();
const auto broadcast = peer->monoforumBroadcast();
const auto channelName = (broadcast ? broadcast : peer)->name();
const auto amount = admin
? HistoryView::PriceAfterCommission(session, price)
: price;
const auto commission = HistoryView::FormatAfterCommissionPercent(
session,
price);
const auto date = langDateTime(base::unixtime::parse(scheduleDate));
show->show(Box([=](not_null<Ui::GenericBox*> box) {
const auto callback = std::make_shared<Fn<void()>>();
auto text = admin
? tr::lng_suggest_accept_text(
tr::now,
lt_from,
tr::bold(item->from()->shortName()),
tr::marked)
: tr::lng_suggest_accept_text_to(
tr::now,
lt_channel,
tr::bold(channelName),
tr::marked);
if (price) {
text.append("\n\n").append(admin
? (scheduleDate
? (amount.stars()
? tr::lng_suggest_accept_receive_stars
: tr::lng_suggest_accept_receive_ton)(
tr::now,
lt_count_decimal,
amount.value(),
lt_channel,
tr::bold(channelName),
lt_percent,
TextWithEntities{ commission },
lt_date,
tr::bold(date),
tr::rich)
: (amount.stars()
? tr::lng_suggest_accept_receive_now_stars
: tr::lng_suggest_accept_receive_now_ton)(
tr::now,
lt_count_decimal,
amount.value(),
lt_channel,
tr::bold(channelName),
lt_percent,
TextWithEntities{ commission },
tr::rich))
: (scheduleDate
? (amount.stars()
? tr::lng_suggest_accept_pay_stars
: tr::lng_suggest_accept_pay_ton)(
tr::now,
lt_count_decimal,
amount.value(),
lt_date,
tr::bold(date),
tr::rich)
: (amount.stars()
? tr::lng_suggest_accept_pay_now_stars
: tr::lng_suggest_accept_pay_now_ton)(
tr::now,
lt_count_decimal,
amount.value(),
tr::rich)));
if (admin) {
text.append(' ').append(
tr::lng_suggest_accept_receive_if(
tr::now,
tr::rich));
if (price.stars()) {
text.append("\n\n").append(
tr::lng_suggest_options_stars_warning(
tr::now,
tr::rich));
}
}
}
Ui::ConfirmBox(box, {
.text = text,
.confirmed = [=](Fn<void()> close) { (*callback)(); close(); },
.confirmText = tr::lng_suggest_accept_send(),
.title = tr::lng_suggest_accept_title(),
});
*callback = [=, weak = base::make_weak(box)] {
if (const auto onstack = accepted) {
onstack();
}
const auto item = show->session().data().message(id);
if (!item) {
return;
}
SendApproval(show, item, scheduleDate);
if (const auto strong = weak.get()) {
strong->closeBox();
}
};
}));
}
void SendDecline(
std::shared_ptr<Main::SessionShow> show,
not_null<HistoryItem*> item,
const QString &comment) {
using Flag = MTPmessages_ToggleSuggestedPostApproval::Flag;
const auto suggestion = item->Get<HistoryMessageSuggestion>();
if (!suggestion
|| suggestion->accepted
|| suggestion->rejected
|| suggestion->requestId) {
return;
}
const auto id = item->fullId();
const auto session = &show->session();
const auto finish = [=] {
if (const auto item = session->data().message(id)) {
const auto suggestion = item->Get<HistoryMessageSuggestion>();
if (suggestion) {
suggestion->requestId = 0;
}
}
};
suggestion->requestId = session->api().request(
MTPmessages_ToggleSuggestedPostApproval(
MTP_flags(Flag::f_reject
| (comment.isEmpty() ? Flag() : Flag::f_reject_comment)),
item->history()->peer->input(),
MTP_int(item->id.bare),
MTPint(), // schedule_date
MTP_string(comment))
).done([=](const MTPUpdates &result) {
session->api().applyUpdates(result);
finish();
}).fail([=](const MTP::Error &error) {
show->showToast(error.type());
finish();
}).send();
}
void RequestApprovalDate(
std::shared_ptr<Main::SessionShow> show,
not_null<HistoryItem*> item) {
const auto id = item->fullId();
const auto weak = std::make_shared<base::weak_qptr<Ui::BoxContent>>();
const auto close = [=] {
if (const auto strong = weak->get()) {
strong->closeBox();
}
};
const auto done = [=](TimeId result) {
if (const auto item = show->session().data().message(id)) {
ConfirmApproval(show, item, result, close);
} else {
close();
}
};
using namespace HistoryView;
auto dateBox = Box(ChooseSuggestTimeBox, SuggestTimeBoxArgs{
.session = &show->session(),
.done = done,
.mode = SuggestMode::Publish,
});
*weak = dateBox.data();
show->show(std::move(dateBox));
}
void RequestDeclineComment(
std::shared_ptr<Main::SessionShow> show,
not_null<HistoryItem*> item) {
const auto id = item->fullId();
const auto admin = item->history()->amMonoforumAdmin();
const auto peer = item->history()->peer;
const auto broadcast = peer->monoforumBroadcast();
const auto channelName = (broadcast ? broadcast : peer)->name();
show->show(Box([=](not_null<Ui::GenericBox*> box) {
const auto callback = std::make_shared<Fn<void()>>();
Ui::ConfirmBox(box, {
.text = (admin
? tr::lng_suggest_decline_text(
lt_from,
rpl::single(tr::bold(item->from()->shortName())),
tr::marked)
: tr::lng_suggest_decline_text_to(
lt_channel,
rpl::single(tr::bold(channelName)),
tr::marked)),
.confirmed = [=](Fn<void()> close) { (*callback)(); close(); },
.confirmText = tr::lng_suggest_action_decline(),
.confirmStyle = &st::attentionBoxButton,
.title = tr::lng_suggest_decline_title(),
});
const auto reason = box->addRow(object_ptr<Ui::InputField>(
box,
st::factcheckField,
Ui::InputField::Mode::NoNewlines,
tr::lng_suggest_decline_reason()));
box->setFocusCallback([=] {
reason->setFocusFast();
});
*callback = [=, weak = base::make_weak(box)] {
const auto item = show->session().data().message(id);
if (!item) {
return;
}
SendDecline(show, item, reason->getLastText().trimmed());
if (const auto strong = weak.get()) {
strong->closeBox();
}
};
reason->submits(
) | rpl::on_next([=](Qt::KeyboardModifiers modifiers) {
if (!(modifiers & Qt::ShiftModifier)) {
(*callback)();
}
}, box->lifetime());
}));
}
struct SendSuggestState {
SendPaymentHelper sendPayment;
};
void SendSuggest(
std::shared_ptr<Main::SessionShow> show,
not_null<HistoryItem*> item,
std::shared_ptr<SendSuggestState> state,
Fn<void(SuggestOptions&)> modify,
Fn<void()> done = nullptr,
int starsApproved = 0) {
const auto suggestion = item->Get<HistoryMessageSuggestion>();
const auto id = item->fullId();
const auto withPaymentApproved = [=](int stars) {
if (const auto item = show->session().data().message(id)) {
SendSuggest(show, item, state, modify, done, stars);
}
};
const auto isForward = item->Get<HistoryMessageForwarded>();
auto action = SendAction(item->history());
action.options.suggest.exists = 1;
if (suggestion) {
action.options.suggest.date = suggestion->date;
action.options.suggest.priceWhole = suggestion->price.whole();
action.options.suggest.priceNano = suggestion->price.nano();
action.options.suggest.ton = suggestion->price.ton() ? 1 : 0;
}
modify(action.options.suggest);
action.options.starsApproved = starsApproved;
action.replyTo.monoforumPeerId = item->history()->amMonoforumAdmin()
? item->sublistPeerId()
: PeerId();
action.replyTo.messageId = item->fullId();
const auto checked = state->sendPayment.check(
show,
item->history()->peer,
action.options,
1,
withPaymentApproved);
if (!checked) {
return;
}
show->session().api().sendAction(action);
show->session().api().forwardMessages({
.items = { item },
.options = (isForward
? Data::ForwardOptions::PreserveInfo
: Data::ForwardOptions::NoSenderNames),
}, action);
if (const auto onstack = done) {
onstack();
}
}
void SuggestApprovalDate(
std::shared_ptr<Main::SessionShow> show,
not_null<HistoryItem*> item) {
const auto suggestion = item->Get<HistoryMessageSuggestion>();
if (!suggestion) {
return;
}
const auto id = item->fullId();
const auto state = std::make_shared<SendSuggestState>();
const auto weak = std::make_shared<base::weak_qptr<Ui::BoxContent>>();
const auto done = [=](TimeId result) {
const auto item = show->session().data().message(id);
if (!item) {
return;
}
const auto close = [=] {
if (const auto strong = weak->get()) {
strong->closeBox();
}
};
SendSuggest(
show,
item,
state,
[=](SuggestOptions &options) { options.date = result; },
close);
};
using namespace HistoryView;
auto dateBox = Box(ChooseSuggestTimeBox, SuggestTimeBoxArgs{
.session = &show->session(),
.done = done,
.value = suggestion->date,
.mode = SuggestMode::Change,
});
*weak = dateBox.data();
show->show(std::move(dateBox));
}
void SuggestOfferForMessage(
std::shared_ptr<Main::SessionShow> show,
not_null<HistoryItem*> item,
SuggestOptions values,
HistoryView::SuggestMode mode) {
const auto id = item->fullId();
const auto state = std::make_shared<SendSuggestState>();
const auto weak = std::make_shared<base::weak_qptr<Ui::BoxContent>>();
const auto done = [=](SuggestOptions result) {
const auto item = show->session().data().message(id);
if (!item) {
return;
}
const auto close = [=] {
if (const auto strong = weak->get()) {
strong->closeBox();
}
};
SendSuggest(
show,
item,
state,
[=](SuggestOptions &options) { options = result; },
close);
};
using namespace HistoryView;
auto priceBox = Box(ChooseSuggestPriceBox, SuggestPriceBoxArgs{
.peer = item->history()->peer,
.done = done,
.value = values,
.mode = mode,
});
*weak = priceBox.data();
show->show(std::move(priceBox));
}
void SuggestApprovalPrice(
std::shared_ptr<Main::SessionShow> show,
not_null<HistoryItem*> item) {
const auto suggestion = item->Get<HistoryMessageSuggestion>();
if (!suggestion) {
return;
}
using namespace HistoryView;
SuggestOfferForMessage(show, item, {
.exists = uint32(1),
.priceWhole = uint32(suggestion->price.whole()),
.priceNano = uint32(suggestion->price.nano()),
.ton = uint32(suggestion->price.ton() ? 1 : 0),
.date = suggestion->date,
}, SuggestMode::Change);
}
void ConfirmGiftSaleAccept(
not_null<Window::SessionController*> window,
not_null<HistoryItem*> item,
not_null<HistoryMessageSuggestion*> suggestion) {
ShowGiftSaleAcceptBox(window, item, suggestion);
}
void ConfirmGiftSaleDecline(
not_null<Window::SessionController*> window,
not_null<HistoryItem*> item,
not_null<HistoryMessageSuggestion*> suggestion) {
ShowGiftSaleRejectBox(window, item, suggestion);
}
} // namespace
std::shared_ptr<ClickHandler> AcceptClickHandler(
not_null<HistoryItem*> item) {
const auto session = &item->history()->session();
const auto id = item->fullId();
return std::make_shared<LambdaClickHandler>([=](ClickContext context) {
const auto my = context.other.value<ClickHandlerContext>();
const auto controller = my.sessionWindow.get();
if (!controller || &controller->session() != session) {
return;
}
const auto item = session->data().message(id);
if (!item) {
return;
}
const auto show = controller->uiShow();
const auto suggestion = item->Get<HistoryMessageSuggestion>();
if (!suggestion) {
return;
} else if (suggestion->gift) {
ConfirmGiftSaleAccept(controller, item, suggestion);
} else if (!suggestion->date) {
RequestApprovalDate(show, item);
} else {
ConfirmApproval(show, item);
}
});
}
std::shared_ptr<ClickHandler> DeclineClickHandler(
not_null<HistoryItem*> item) {
const auto session = &item->history()->session();
const auto id = item->fullId();
return std::make_shared<LambdaClickHandler>([=](ClickContext context) {
const auto my = context.other.value<ClickHandlerContext>();
const auto controller = my.sessionWindow.get();
if (!controller || &controller->session() != session) {
return;
}
const auto item = session->data().message(id);
if (!item) {
return;
}
const auto suggestion = item->Get<HistoryMessageSuggestion>();
if (suggestion && suggestion->gift) {
ConfirmGiftSaleDecline(controller, item, suggestion);
} else {
RequestDeclineComment(controller->uiShow(), item);
}
});
}
std::shared_ptr<ClickHandler> SuggestChangesClickHandler(
not_null<HistoryItem*> item) {
const auto session = &item->history()->session();
const auto id = item->fullId();
return std::make_shared<LambdaClickHandler>([=](ClickContext context) {
const auto my = context.other.value<ClickHandlerContext>();
const auto window = my.sessionWindow.get();
if (!window || &window->session() != session) {
return;
}
const auto item = session->data().message(id);
if (!item) {
return;
}
const auto menu = Ui::CreateChild<Ui::PopupMenu>(
window->widget(),
st::popupMenuWithIcons);
if (HistoryView::CanEditSuggestedMessage(item)) {
menu->addAction(tr::lng_suggest_menu_edit_message(tr::now), [=] {
const auto item = session->data().message(id);
if (!item) {
return;
}
const auto suggestion = item->Get<HistoryMessageSuggestion>();
if (!suggestion) {
return;
}
const auto history = item->history();
const auto editData = PrepareEditText(item);
const auto cursor = MessageCursor{
int(editData.text.size()),
int(editData.text.size()),
Ui::kQFixedMax
};
const auto monoforumPeerId = history->amMonoforumAdmin()
? item->sublistPeerId()
: PeerId();
const auto previewDraft = Data::WebPageDraft::FromItem(item);
history->setLocalEditDraft(std::make_unique<Data::Draft>(
editData,
FullReplyTo{
.messageId = FullMsgId(history->peer->id, item->id),
.monoforumPeerId = monoforumPeerId,
},
SuggestOptions{
.exists = uint32(1),
.priceWhole = uint32(suggestion->price.whole()),
.priceNano = uint32(suggestion->price.nano()),
.ton = uint32(suggestion->price.ton() ? 1 : 0),
.date = suggestion->date,
},
cursor,
previewDraft));
history->session().changes().entryUpdated(
(monoforumPeerId
? item->savedSublist()
: (Data::Thread*)history.get()),
Data::EntryUpdate::Flag::LocalDraftSet);
}, &st::menuIconEdit);
}
menu->addAction(tr::lng_suggest_menu_edit_price(tr::now), [=] {
if (const auto item = session->data().message(id)) {
SuggestApprovalPrice(window->uiShow(), item);
}
}, &st::menuIconTagSell);
menu->addAction(tr::lng_suggest_menu_edit_time(tr::now), [=] {
if (const auto item = session->data().message(id)) {
SuggestApprovalDate(window->uiShow(), item);
}
}, &st::menuIconSchedule);
menu->popup(QCursor::pos());
});
}
void AddOfferToMessage(
std::shared_ptr<Main::SessionShow> show,
FullMsgId itemId) {
const auto session = &show->session();
const auto item = session->data().message(itemId);
if (!item || !HistoryView::CanAddOfferToMessage(item)) {
return;
}
SuggestOfferForMessage(show, item, {}, HistoryView::SuggestMode::New);
}
} // namespace Api

View File

@@ -0,0 +1,29 @@
/*
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
*/
#pragma once
class ClickHandler;
namespace Main {
class SessionShow;
} // namespace Main
namespace Api {
[[nodiscard]] std::shared_ptr<ClickHandler> AcceptClickHandler(
not_null<HistoryItem*> item);
[[nodiscard]] std::shared_ptr<ClickHandler> DeclineClickHandler(
not_null<HistoryItem*> item);
[[nodiscard]] std::shared_ptr<ClickHandler> SuggestChangesClickHandler(
not_null<HistoryItem*> item);
void AddOfferToMessage(
std::shared_ptr<Main::SessionShow> show,
FullMsgId itemId);
} // namespace Api

View File

@@ -0,0 +1,376 @@
/*
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 "api/api_text_entities.h"
#include "data/data_document.h"
#include "data/data_session.h"
#include "data/data_user.h"
#include "data/stickers/data_custom_emoji.h"
#include "data/stickers/data_stickers_set.h"
#include "history/history.h"
#include "history/history_item.h"
#include "main/main_session.h"
namespace Api {
namespace {
using namespace TextUtilities;
[[nodiscard]] QString CustomEmojiEntityData(
const MTPDmessageEntityCustomEmoji &data) {
return Data::SerializeCustomEmojiId(data.vdocument_id().v);
}
[[nodiscard]] std::optional<MTPMessageEntity> CustomEmojiEntity(
MTPint offset,
MTPint length,
const QString &data) {
const auto parsed = Data::ParseCustomEmojiData(data);
if (!parsed) {
return {};
}
return MTP_messageEntityCustomEmoji(
offset,
length,
MTP_long(parsed));
}
[[nodiscard]] std::optional<MTPMessageEntity> MentionNameEntity(
not_null<Main::Session*> session,
MTPint offset,
MTPint length,
const QString &data) {
const auto parsed = MentionNameDataToFields(data);
if (!parsed.userId || parsed.selfId != session->userId().bare) {
return {};
}
const auto user = session->data().user(UserId(parsed.userId));
const auto item = user->isLoaded()
? nullptr
: user->owner().messageWithPeer(user->id);
const auto input = item
? MTP_inputUserFromMessage(
item->history()->peer->input(),
MTP_int(item->id.bare),
MTP_long(parsed.userId))
: (parsed.userId == parsed.selfId)
? MTP_inputUserSelf()
: user->isLoaded()
? user->inputUser()
: MTP_inputUser(
MTP_long(parsed.userId),
MTP_long(parsed.accessHash));
return MTP_inputMessageEntityMentionName(offset, length, input);
}
} // namespace
EntitiesInText EntitiesFromMTP(
Main::Session *session,
const QVector<MTPMessageEntity> &entities) {
if (entities.isEmpty()) {
return {};
}
auto result = EntitiesInText();
result.reserve(entities.size());
for (const auto &entity : entities) {
entity.match([&](const MTPDmessageEntityUnknown &d) {
}, [&](const MTPDmessageEntityMention &d) {
result.push_back({
EntityType::Mention,
d.voffset().v,
d.vlength().v,
});
}, [&](const MTPDmessageEntityHashtag &d) {
result.push_back({
EntityType::Hashtag,
d.voffset().v,
d.vlength().v,
});
}, [&](const MTPDmessageEntityBotCommand &d) {
result.push_back({
EntityType::BotCommand,
d.voffset().v,
d.vlength().v,
});
}, [&](const MTPDmessageEntityUrl &d) {
result.push_back({
EntityType::Url,
d.voffset().v,
d.vlength().v,
});
}, [&](const MTPDmessageEntityEmail &d) {
result.push_back({
EntityType::Email,
d.voffset().v,
d.vlength().v,
});
}, [&](const MTPDmessageEntityBold &d) {
result.push_back({
EntityType::Bold,
d.voffset().v,
d.vlength().v,
});
}, [&](const MTPDmessageEntityItalic &d) {
result.push_back({
EntityType::Italic,
d.voffset().v,
d.vlength().v,
});
}, [&](const MTPDmessageEntityCode &d) {
result.push_back({
EntityType::Code,
d.voffset().v,
d.vlength().v,
});
}, [&](const MTPDmessageEntityPre &d) {
result.push_back({
EntityType::Pre,
d.voffset().v,
d.vlength().v,
qs(d.vlanguage()),
});
}, [&](const MTPDmessageEntityTextUrl &d) {
result.push_back({
EntityType::CustomUrl,
d.voffset().v,
d.vlength().v,
qs(d.vurl()),
});
}, [&](const MTPDmessageEntityMentionName &d) {
if (!session) {
return;
}
const auto userId = UserId(d.vuser_id());
const auto user = session->data().userLoaded(userId);
const auto data = MentionNameDataFromFields({
.selfId = session->userId().bare,
.userId = userId.bare,
.accessHash = user ? user->accessHash() : 0,
});
result.push_back({
EntityType::MentionName,
d.voffset().v,
d.vlength().v,
data,
});
}, [&](const MTPDinputMessageEntityMentionName &d) {
if (!session) {
return;
}
const auto data = d.vuser_id().match([&](
const MTPDinputUserSelf &) {
return MentionNameDataFromFields({
.selfId = session->userId().bare,
.userId = session->userId().bare,
.accessHash = session->user()->accessHash(),
});
}, [&](const MTPDinputUser &data) {
return MentionNameDataFromFields({
.selfId = session->userId().bare,
.userId = UserId(data.vuser_id()).bare,
.accessHash = data.vaccess_hash().v,
});
}, [](const auto &) {
return QString();
});
if (!data.isEmpty()) {
result.push_back({
EntityType::MentionName,
d.voffset().v,
d.vlength().v,
data,
});
}
}, [&](const MTPDmessageEntityPhone &d) {
result.push_back({
EntityType::Phone,
d.voffset().v,
d.vlength().v,
});
}, [&](const MTPDmessageEntityCashtag &d) {
result.push_back({
EntityType::Cashtag,
d.voffset().v,
d.vlength().v,
});
}, [&](const MTPDmessageEntityUnderline &d) {
result.push_back({
EntityType::Underline,
d.voffset().v,
d.vlength().v,
});
}, [&](const MTPDmessageEntityStrike &d) {
result.push_back({
EntityType::StrikeOut,
d.voffset().v,
d.vlength().v,
});
}, [&](const MTPDmessageEntityBankCard &d) {
result.push_back({
EntityType::BankCard,
d.voffset().v,
d.vlength().v,
});
}, [&](const MTPDmessageEntitySpoiler &d) {
result.push_back({
EntityType::Spoiler,
d.voffset().v,
d.vlength().v,
});
}, [&](const MTPDmessageEntityCustomEmoji &d) {
result.push_back({
EntityType::CustomEmoji,
d.voffset().v,
d.vlength().v,
CustomEmojiEntityData(d),
});
}, [&](const MTPDmessageEntityBlockquote &d) {
result.push_back({
EntityType::Blockquote,
d.voffset().v,
d.vlength().v,
d.is_collapsed() ? u"1"_q : QString(),
});
});
}
return result;
}
MTPVector<MTPMessageEntity> EntitiesToMTP(
Main::Session *session,
const EntitiesInText &entities,
ConvertOption option) {
auto v = QVector<MTPMessageEntity>();
v.reserve(entities.size());
for (const auto &entity : entities) {
if (entity.length() <= 0) {
continue;
}
if (option == ConvertOption::SkipLocal
&& entity.type() != EntityType::Bold
//&& entity.type() != EntityType::Semibold // Not in API.
&& entity.type() != EntityType::Italic
&& entity.type() != EntityType::Underline
&& entity.type() != EntityType::StrikeOut
&& entity.type() != EntityType::Code // #TODO entities
&& entity.type() != EntityType::Pre
&& entity.type() != EntityType::Blockquote
&& entity.type() != EntityType::Spoiler
&& entity.type() != EntityType::MentionName
&& entity.type() != EntityType::CustomUrl
&& entity.type() != EntityType::CustomEmoji) {
continue;
}
auto offset = MTP_int(entity.offset());
auto length = MTP_int(entity.length());
switch (entity.type()) {
case EntityType::Url: {
v.push_back(MTP_messageEntityUrl(offset, length));
} break;
case EntityType::CustomUrl: {
v.push_back(
MTP_messageEntityTextUrl(
offset,
length,
MTP_string(entity.data())));
} break;
case EntityType::Email: {
v.push_back(MTP_messageEntityEmail(offset, length));
} break;
case EntityType::Phone: {
v.push_back(MTP_messageEntityPhone(offset, length));
} break;
case EntityType::BankCard: {
v.push_back(MTP_messageEntityBankCard(offset, length));
} break;
case EntityType::Hashtag: {
v.push_back(MTP_messageEntityHashtag(offset, length));
} break;
case EntityType::Cashtag: {
v.push_back(MTP_messageEntityCashtag(offset, length));
} break;
case EntityType::Mention: {
v.push_back(MTP_messageEntityMention(offset, length));
} break;
case EntityType::MentionName: {
Assert(session != nullptr);
const auto valid = MentionNameEntity(
session,
offset,
length,
entity.data());
if (valid) {
v.push_back(*valid);
}
} break;
case EntityType::BotCommand: {
v.push_back(MTP_messageEntityBotCommand(offset, length));
} break;
case EntityType::Bold: {
v.push_back(MTP_messageEntityBold(offset, length));
} break;
case EntityType::Italic: {
v.push_back(MTP_messageEntityItalic(offset, length));
} break;
case EntityType::Underline: {
v.push_back(MTP_messageEntityUnderline(offset, length));
} break;
case EntityType::StrikeOut: {
v.push_back(MTP_messageEntityStrike(offset, length));
} break;
case EntityType::Code: {
// #TODO entities.
v.push_back(MTP_messageEntityCode(offset, length));
} break;
case EntityType::Pre: {
v.push_back(
MTP_messageEntityPre(
offset,
length,
MTP_string(entity.data())));
} break;
case EntityType::Blockquote: {
using Flag = MTPDmessageEntityBlockquote::Flag;
const auto collapsed = !entity.data().isEmpty();
v.push_back(
MTP_messageEntityBlockquote(
MTP_flags(collapsed ? Flag::f_collapsed : Flag()),
offset,
length));
} break;
case EntityType::Spoiler: {
v.push_back(MTP_messageEntitySpoiler(offset, length));
} break;
case EntityType::CustomEmoji: {
const auto valid = CustomEmojiEntity(
offset,
length,
entity.data());
if (valid) {
v.push_back(*valid);
}
} break;
}
}
return MTP_vector<MTPMessageEntity>(std::move(v));
}
TextWithEntities ParseTextWithEntities(
Main::Session *session,
const MTPTextWithEntities &text) {
const auto &data = text.data();
return {
.text = qs(data.vtext()),
.entities = EntitiesFromMTP(session, data.ventities().v),
};
}
} // namespace Api

View File

@@ -0,0 +1,36 @@
/*
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
*/
#pragma once
#include "ui/text/text_entity.h"
namespace Main {
class Session;
} // namespace Main
namespace Api {
enum class ConvertOption {
WithLocal,
SkipLocal,
};
[[nodiscard]] EntitiesInText EntitiesFromMTP(
Main::Session *session,
const QVector<MTPMessageEntity> &entities);
[[nodiscard]] MTPVector<MTPMessageEntity> EntitiesToMTP(
Main::Session *session,
const EntitiesInText &entities,
ConvertOption option = ConvertOption::WithLocal);
[[nodiscard]] TextWithEntities ParseTextWithEntities(
Main::Session *session,
const MTPTextWithEntities &text);
} // namespace Api

View File

@@ -0,0 +1,257 @@
/*
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 "api/api_todo_lists.h"
#include "api/api_editing.h"
#include "apiwrap.h"
#include "base/random.h"
#include "data/business/data_shortcut_messages.h" // ShortcutIdToMTP
#include "data/data_changes.h"
#include "data/data_histories.h"
#include "data/data_todo_list.h"
#include "data/data_session.h"
#include "history/history.h"
#include "history/history_item.h"
#include "history/history_item_helpers.h" // ShouldSendSilent
#include "main/main_session.h"
namespace Api {
namespace {
constexpr auto kSendTogglesDelay = 3 * crl::time(1000);
} // namespace
TodoLists::TodoLists(not_null<ApiWrap*> api)
: _session(&api->session())
, _api(&api->instance())
, _sendTimer([=] { sendAccumulatedToggles(false); }) {
}
void TodoLists::create(
const TodoListData &data,
SendAction action,
Fn<void()> done,
Fn<void(QString)> fail) {
_session->api().sendAction(action);
const auto history = action.history;
const auto peer = history->peer;
const auto topicRootId = action.replyTo.messageId
? action.replyTo.topicRootId
: 0;
const auto monoforumPeerId = action.replyTo.monoforumPeerId;
auto sendFlags = MTPmessages_SendMedia::Flags(0);
if (action.replyTo) {
sendFlags |= MTPmessages_SendMedia::Flag::f_reply_to;
}
const auto clearCloudDraft = action.clearDraft;
if (clearCloudDraft) {
sendFlags |= MTPmessages_SendMedia::Flag::f_clear_draft;
history->clearLocalDraft(topicRootId, monoforumPeerId);
history->clearCloudDraft(topicRootId, monoforumPeerId);
history->startSavingCloudDraft(topicRootId, monoforumPeerId);
}
const auto silentPost = ShouldSendSilent(peer, action.options);
const auto starsPaid = std::min(
peer->starsPerMessageChecked(),
action.options.starsApproved);
if (silentPost) {
sendFlags |= MTPmessages_SendMedia::Flag::f_silent;
}
if (action.options.scheduled) {
sendFlags |= MTPmessages_SendMedia::Flag::f_schedule_date;
if (action.options.scheduleRepeatPeriod) {
sendFlags |= MTPmessages_SendMedia::Flag::f_schedule_repeat_period;
}
}
if (action.options.shortcutId) {
sendFlags |= MTPmessages_SendMedia::Flag::f_quick_reply_shortcut;
}
if (action.options.effectId) {
sendFlags |= MTPmessages_SendMedia::Flag::f_effect;
}
if (action.options.suggest) {
sendFlags |= MTPmessages_SendMedia::Flag::f_suggested_post;
}
if (starsPaid) {
action.options.starsApproved -= starsPaid;
sendFlags |= MTPmessages_SendMedia::Flag::f_allow_paid_stars;
}
const auto sendAs = action.options.sendAs;
if (sendAs) {
sendFlags |= MTPmessages_SendMedia::Flag::f_send_as;
}
auto &histories = history->owner().histories();
const auto randomId = base::RandomValue<uint64>();
histories.sendPreparedMessage(
history,
action.replyTo,
randomId,
Data::Histories::PrepareMessage<MTPmessages_SendMedia>(
MTP_flags(sendFlags),
peer->input(),
Data::Histories::ReplyToPlaceholder(),
TodoListDataToInputMedia(&data),
MTP_string(),
MTP_long(randomId),
MTPReplyMarkup(),
MTPVector<MTPMessageEntity>(),
MTP_int(action.options.scheduled),
MTP_int(action.options.scheduleRepeatPeriod),
(sendAs ? sendAs->input() : MTP_inputPeerEmpty()),
Data::ShortcutIdToMTP(_session, action.options.shortcutId),
MTP_long(action.options.effectId),
MTP_long(starsPaid),
SuggestToMTP(action.options.suggest)
), [=](const MTPUpdates &result, const MTP::Response &response) {
if (clearCloudDraft) {
history->finishSavingCloudDraft(
topicRootId,
monoforumPeerId,
UnixtimeFromMsgId(response.outerMsgId));
}
_session->changes().historyUpdated(
history,
(action.options.scheduled
? Data::HistoryUpdate::Flag::ScheduledSent
: Data::HistoryUpdate::Flag::MessageSent));
if (const auto onstack = done) {
onstack();
}
}, [=](const MTP::Error &error, const MTP::Response &response) {
if (clearCloudDraft) {
history->finishSavingCloudDraft(
topicRootId,
monoforumPeerId,
UnixtimeFromMsgId(response.outerMsgId));
}
if (const auto onstack = fail) {
onstack(error.type());
}
});
}
void TodoLists::edit(
not_null<HistoryItem*> item,
const TodoListData &data,
SendOptions options,
Fn<void()> done,
Fn<void(QString)> fail) {
EditTodoList(item, data, options, [=](mtpRequestId) {
if (const auto onstack = done) {
onstack();
}
}, [=](const QString &error, mtpRequestId) {
if (const auto onstack = fail) {
onstack(error);
}
});
}
void TodoLists::add(
not_null<HistoryItem*> item,
const std::vector<TodoListItem> &items,
Fn<void()> done,
Fn<void(QString)> fail) {
if (items.empty()) {
return;
}
const auto session = _session;
_session->api().request(MTPmessages_AppendTodoList(
item->history()->peer->input(),
MTP_int(item->id.bare),
TodoListItemsToMTP(&item->history()->session(), items)
)).done([=](const MTPUpdates &result) {
session->api().applyUpdates(result);
if (const auto onstack = done) {
onstack();
}
}).fail([=](const MTP::Error &error) {
if (const auto onstack = fail) {
onstack(error.type());
}
}).send();
}
void TodoLists::toggleCompletion(FullMsgId itemId, int id, bool completed) {
auto &entry = _toggles[itemId];
if (completed) {
const auto changed1 = entry.completed.emplace(id).second;
const auto changed2 = entry.incompleted.remove(id);
if (!changed1 && !changed2) {
return;
}
} else {
const auto changed1 = entry.incompleted.emplace(id).second;
const auto changed2 = entry.completed.remove(id);
if (!changed1 && !changed2) {
return;
}
}
entry.scheduled = crl::now();
if (!entry.requestId && !_sendTimer.isActive()) {
_sendTimer.callOnce(kSendTogglesDelay);
}
}
void TodoLists::sendAccumulatedToggles(bool force) {
const auto now = crl::now();
auto nearest = crl::time(0);
for (auto &[itemId, entry] : _toggles) {
if (entry.requestId) {
continue;
}
const auto wait = entry.scheduled + kSendTogglesDelay - now;
if (wait <= 0) {
entry.scheduled = 0;
send(itemId, entry);
} else if (!nearest || nearest > wait) {
nearest = wait;
}
}
if (nearest > 0) {
_sendTimer.callOnce(nearest);
}
}
void TodoLists::send(FullMsgId itemId, Accumulated &entry) {
const auto item = _session->data().message(itemId);
if (!item) {
return;
}
auto completed = entry.completed
| ranges::views::transform([](int id) { return MTP_int(id); });
auto incompleted = entry.incompleted
| ranges::views::transform([](int id) { return MTP_int(id); });
entry.requestId = _api.request(MTPmessages_ToggleTodoCompleted(
item->history()->peer->input(),
MTP_int(item->id),
MTP_vector_from_range(completed),
MTP_vector_from_range(incompleted)
)).done([=](const MTPUpdates &result) {
_session->api().applyUpdates(result);
finishRequest(itemId);
}).fail([=](const MTP::Error &error) {
finishRequest(itemId);
}).send();
entry.completed.clear();
entry.incompleted.clear();
}
void TodoLists::finishRequest(FullMsgId itemId) {
auto &entry = _toggles[itemId];
entry.requestId = 0;
if (entry.completed.empty() && entry.incompleted.empty()) {
_toggles.remove(itemId);
} else {
sendAccumulatedToggles(false);
}
}
} // namespace Api

View File

@@ -0,0 +1,69 @@
/*
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
*/
#pragma once
#include "base/timer.h"
#include "mtproto/sender.h"
class ApiWrap;
class HistoryItem;
struct TodoListItem;
struct TodoListData;
namespace Main {
class Session;
} // namespace Main
namespace Api {
struct SendAction;
struct SendOptions;
class TodoLists final {
public:
explicit TodoLists(not_null<ApiWrap*> api);
void create(
const TodoListData &data,
SendAction action,
Fn<void()> done,
Fn<void(QString)> fail);
void edit(
not_null<HistoryItem*> item,
const TodoListData &data,
SendOptions options,
Fn<void()> done,
Fn<void(QString)> fail);
void add(
not_null<HistoryItem*> item,
const std::vector<TodoListItem> &items,
Fn<void()> done,
Fn<void(QString)> fail);
void toggleCompletion(FullMsgId itemId, int id, bool completed);
private:
struct Accumulated {
base::flat_set<int> completed;
base::flat_set<int> incompleted;
crl::time scheduled = 0;
mtpRequestId requestId = 0;
};
void sendAccumulatedToggles(bool force);
void send(FullMsgId itemId, Accumulated &entry);
void finishRequest(FullMsgId itemId);
const not_null<Main::Session*> _session;
MTP::Sender _api;
base::flat_map<FullMsgId, Accumulated> _toggles;
base::Timer _sendTimer;
};
} // namespace Api

View File

@@ -0,0 +1,141 @@
/*
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 "api/api_toggling_media.h"
#include "apiwrap.h"
#include "data/data_document.h"
#include "data/data_file_origin.h"
#include "data/data_session.h"
#include "data/stickers/data_stickers.h"
#include "window/window_session_controller.h"
#include "main/main_session.h"
namespace Api {
namespace {
template <typename ToggleRequestCallback, typename DoneCallback>
void ToggleExistingMedia(
not_null<DocumentData*> document,
Data::FileOrigin origin,
ToggleRequestCallback toggleRequest,
DoneCallback &&done) {
const auto api = &document->session().api();
auto performRequest = [=](const auto &repeatRequest) -> void {
const auto usedFileReference = document->fileReference();
api->request(
toggleRequest()
).done(done).fail([=](const MTP::Error &error) {
if (error.code() == 400
&& error.type().startsWith(u"FILE_REFERENCE_"_q)) {
auto refreshed = [=](const Data::UpdatedFileReferences &d) {
if (document->fileReference() != usedFileReference) {
repeatRequest(repeatRequest);
}
};
api->refreshFileReference(origin, std::move(refreshed));
}
}).send();
};
performRequest(performRequest);
}
} // namespace
void ToggleFavedSticker(
std::shared_ptr<ChatHelpers::Show> show,
not_null<DocumentData*> document,
Data::FileOrigin origin) {
ToggleFavedSticker(
std::move(show),
document,
std::move(origin),
!document->owner().stickers().isFaved(document));
}
void ToggleFavedSticker(
std::shared_ptr<ChatHelpers::Show> show,
not_null<DocumentData*> document,
Data::FileOrigin origin,
bool faved) {
if (faved && !document->sticker()) {
return;
}
auto done = [=] {
document->owner().stickers().setFaved(show, document, faved);
};
ToggleExistingMedia(
document,
std::move(origin),
[=, d = document] {
return MTPmessages_FaveSticker(d->mtpInput(), MTP_bool(!faved));
},
std::move(done));
}
void ToggleRecentSticker(
not_null<DocumentData*> document,
Data::FileOrigin origin,
bool saved) {
if (!document->sticker()) {
return;
}
auto done = [=] {
if (!saved) {
document->owner().stickers().removeFromRecentSet(document);
}
};
ToggleExistingMedia(
document,
std::move(origin),
[=] {
return MTPmessages_SaveRecentSticker(
MTP_flags(MTPmessages_SaveRecentSticker::Flag(0)),
document->mtpInput(),
MTP_bool(!saved));
},
std::move(done));
}
void ToggleSavedGif(
std::shared_ptr<ChatHelpers::Show> show,
not_null<DocumentData*> document,
Data::FileOrigin origin,
bool saved) {
if (saved && !document->isGifv()) {
return;
}
auto done = [=] {
if (saved) {
document->owner().stickers().addSavedGif(show, document);
}
};
ToggleExistingMedia(
document,
std::move(origin),
[=, d = document] {
return MTPmessages_SaveGif(d->mtpInput(), MTP_bool(!saved));
},
std::move(done));
}
void ToggleSavedRingtone(
not_null<DocumentData*> document,
Data::FileOrigin origin,
Fn<void()> &&done,
bool saved) {
ToggleExistingMedia(
document,
std::move(origin),
[=, d = document] {
return MTPaccount_SaveRingtone(d->mtpInput(), MTP_bool(!saved));
},
std::move(done));
}
} // namespace Api

View File

@@ -0,0 +1,44 @@
/*
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
*/
#pragma once
namespace ChatHelpers {
class Show;
} // namespace ChatHelpers
namespace Api {
void ToggleFavedSticker(
std::shared_ptr<ChatHelpers::Show> show,
not_null<DocumentData*> document,
Data::FileOrigin origin);
void ToggleFavedSticker(
std::shared_ptr<ChatHelpers::Show> show,
not_null<DocumentData*> document,
Data::FileOrigin origin,
bool faved);
void ToggleRecentSticker(
not_null<DocumentData*> document,
Data::FileOrigin origin,
bool saved);
void ToggleSavedGif(
std::shared_ptr<ChatHelpers::Show> show,
not_null<DocumentData*> document,
Data::FileOrigin origin,
bool saved);
void ToggleSavedRingtone(
not_null<DocumentData*> document,
Data::FileOrigin origin,
Fn<void()> &&done,
bool saved);
} // namespace Api

View File

@@ -0,0 +1,211 @@
/*
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 "api/api_transcribes.h"
#include "apiwrap.h"
#include "data/data_channel.h"
#include "data/data_document.h"
#include "data/data_peer.h"
#include "data/data_session.h"
#include "history/history.h"
#include "history/history_item.h"
#include "history/history_item_helpers.h"
#include "main/main_app_config.h"
#include "main/main_session.h"
#include "main/main_session_settings.h"
namespace Api {
Transcribes::Transcribes(not_null<ApiWrap*> api)
: _session(&api->session())
, _api(&api->instance()) {
}
bool Transcribes::isRated(not_null<HistoryItem*> item) const {
const auto fullId = item->fullId();
for (const auto &[transcribeId, id] : _ids) {
if (id == fullId) {
return _session->settings().isTranscriptionRated(transcribeId);
}
}
return false;
}
void Transcribes::rate(not_null<HistoryItem*> item, bool isGood) {
const auto fullId = item->fullId();
for (const auto &[transcribeId, id] : _ids) {
if (id == fullId) {
_api.request(MTPmessages_RateTranscribedAudio(
item->history()->peer->input(),
MTP_int(item->id),
MTP_long(transcribeId),
MTP_bool(isGood))).send();
_session->settings().markTranscriptionAsRated(transcribeId);
_session->saveSettings();
return;
}
}
}
bool Transcribes::freeFor(not_null<HistoryItem*> item) const {
if (const auto channel = item->history()->peer->asMegagroup()) {
const auto owner = &channel->owner();
return channel->levelHint() >= owner->groupFreeTranscribeLevel();
}
return false;
}
bool Transcribes::trialsSupport() {
if (!_trialsSupport) {
const auto count = _session->appConfig().get<int>(
u"transcribe_audio_trial_weekly_number"_q,
0);
const auto until = _session->appConfig().get<int>(
u"transcribe_audio_trial_cooldown_until"_q,
0);
_trialsSupport = (count > 0) || (until > 0);
}
return *_trialsSupport;
}
TimeId Transcribes::trialsRefreshAt() {
if (_trialsRefreshAt < 0) {
_trialsRefreshAt = _session->appConfig().get<int>(
u"transcribe_audio_trial_cooldown_until"_q,
0);
}
return _trialsRefreshAt;
}
int Transcribes::trialsCount() {
if (_trialsCount < 0) {
_trialsCount = _session->appConfig().get<int>(
u"transcribe_audio_trial_weekly_number"_q,
-1);
return std::max(_trialsCount, 0);
}
return _trialsCount;
}
crl::time Transcribes::trialsMaxLengthMs() const {
return 1000 * _session->appConfig().get<int>(
u"transcribe_audio_trial_duration_max"_q,
300);
}
void Transcribes::toggle(not_null<HistoryItem*> item) {
const auto id = item->fullId();
auto i = _map.find(id);
if (i == _map.end()) {
load(item);
//_session->data().requestItemRepaint(item);
_session->data().requestItemResize(item);
} else if (!i->second.requestId) {
i->second.shown = !i->second.shown;
if (i->second.roundview) {
_session->data().requestItemViewRefresh(item);
}
_session->data().requestItemResize(item);
}
}
const Transcribes::Entry &Transcribes::entry(
not_null<HistoryItem*> item) const {
static auto empty = Entry();
const auto i = _map.find(item->fullId());
return (i != _map.end()) ? i->second : empty;
}
void Transcribes::apply(const MTPDupdateTranscribedAudio &update) {
const auto id = update.vtranscription_id().v;
const auto i = _ids.find(id);
if (i == _ids.end()) {
return;
}
const auto j = _map.find(i->second);
if (j == _map.end()) {
return;
}
const auto text = qs(update.vtext());
j->second.result = text;
j->second.pending = update.is_pending();
if (const auto item = _session->data().message(i->second)) {
if (j->second.roundview) {
_session->data().requestItemViewRefresh(item);
}
_session->data().requestItemResize(item);
}
}
void Transcribes::load(not_null<HistoryItem*> item) {
if (!item->isHistoryEntry() || item->isLocal()) {
return;
}
const auto toggleRound = [](not_null<HistoryItem*> item, Entry &entry) {
if (const auto media = item->media()) {
if (const auto document = media->document()) {
if (document->isVideoMessage()) {
entry.roundview = true;
document->owner().requestItemViewRefresh(item);
}
}
}
};
const auto id = item->fullId();
const auto requestId = _api.request(MTPmessages_TranscribeAudio(
item->history()->peer->input(),
MTP_int(item->id)
)).done([=](const MTPmessages_TranscribedAudio &result) {
const auto &data = result.data();
{
const auto trialsCountChanged = data.vtrial_remains_num()
&& (_trialsCount != data.vtrial_remains_num()->v);
if (trialsCountChanged) {
_trialsCount = data.vtrial_remains_num()->v;
}
const auto refreshAtChanged = data.vtrial_remains_until_date()
&& (_trialsRefreshAt != data.vtrial_remains_until_date()->v);
if (refreshAtChanged) {
_trialsRefreshAt = data.vtrial_remains_until_date()->v;
}
if (trialsCountChanged) {
ShowTrialTranscribesToast(_trialsCount, _trialsRefreshAt);
}
}
auto &entry = _map[id];
entry.requestId = 0;
entry.pending = data.is_pending();
entry.result = qs(data.vtext());
_ids.emplace(data.vtranscription_id().v, id);
if (const auto item = _session->data().message(id)) {
toggleRound(item, entry);
_session->data().requestItemResize(item);
}
}).fail([=](const MTP::Error &error) {
auto &entry = _map[id];
entry.requestId = 0;
entry.pending = false;
entry.failed = true;
if (error.type() == u"MSG_VOICE_TOO_LONG"_q) {
entry.toolong = true;
}
if (const auto item = _session->data().message(id)) {
toggleRound(item, entry);
_session->data().requestItemResize(item);
}
}).send();
auto &entry = _map.emplace(id).first->second;
entry.requestId = requestId;
entry.shown = true;
entry.failed = false;
entry.pending = false;
}
} // namespace Api

View File

@@ -0,0 +1,63 @@
/*
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
*/
#pragma once
#include "mtproto/sender.h"
class ApiWrap;
namespace Main {
class Session;
} // namespace Main
namespace Api {
class Transcribes final {
public:
explicit Transcribes(not_null<ApiWrap*> api);
struct Entry {
QString result;
bool shown = false;
bool failed = false;
bool toolong = false;
bool pending = false;
bool roundview = false;
mtpRequestId requestId = 0;
};
void toggle(not_null<HistoryItem*> item);
[[nodiscard]] const Entry &entry(not_null<HistoryItem*> item) const;
void apply(const MTPDupdateTranscribedAudio &update);
[[nodiscard]] bool freeFor(not_null<HistoryItem*> item) const;
[[nodiscard]] bool isRated(not_null<HistoryItem*> item) const;
void rate(not_null<HistoryItem*> item, bool isGood);
[[nodiscard]] bool trialsSupport();
[[nodiscard]] TimeId trialsRefreshAt();
[[nodiscard]] int trialsCount();
[[nodiscard]] crl::time trialsMaxLengthMs() const;
private:
void load(not_null<HistoryItem*> item);
const not_null<Main::Session*> _session;
MTP::Sender _api;
int _trialsCount = -1;
std::optional<bool> _trialsSupport;
TimeId _trialsRefreshAt = -1;
base::flat_map<FullMsgId, Entry> _map;
base::flat_map<uint64, FullMsgId> _ids;
};
} // namespace Api

View File

@@ -0,0 +1,167 @@
/*
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 "api/api_unread_things.h"
#include "data/data_peer.h"
#include "data/data_channel.h"
#include "data/data_forum_topic.h"
#include "data/data_saved_sublist.h"
#include "data/data_session.h"
#include "main/main_session.h"
#include "history/history.h"
#include "history/history_item.h"
#include "history/history_unread_things.h"
#include "apiwrap.h"
namespace Api {
namespace {
constexpr auto kPreloadIfLess = 5;
constexpr auto kFirstRequestLimit = 10;
constexpr auto kNextRequestLimit = 100;
} // namespace
UnreadThings::UnreadThings(not_null<ApiWrap*> api) : _api(api) {
}
bool UnreadThings::trackMentions(Data::Thread *thread) const {
const auto peer = thread ? thread->peer().get() : nullptr;
return peer
&& (peer->isChat() || peer->isMegagroup())
&& !peer->isMonoforum();
}
bool UnreadThings::trackReactions(Data::Thread *thread) const {
const auto peer = thread ? thread->peer().get() : nullptr;
return peer && (peer->isUser() || peer->isChat() || peer->isMegagroup());
}
void UnreadThings::preloadEnough(Data::Thread *thread) {
if (trackMentions(thread)) {
preloadEnoughMentions(thread);
}
if (trackReactions(thread)) {
preloadEnoughReactions(thread);
}
}
void UnreadThings::mediaAndMentionsRead(
const base::flat_set<MsgId> &readIds,
ChannelData *channel) {
for (const auto &msgId : readIds) {
_api->requestMessageData(channel, msgId, [=] {
const auto item = channel
? _api->session().data().message(channel->id, msgId)
: _api->session().data().nonChannelMessage(msgId);
if (item && item->mentionsMe()) {
item->markMediaAndMentionRead();
}
});
}
}
void UnreadThings::preloadEnoughMentions(not_null<Data::Thread*> thread) {
const auto fullCount = thread->unreadMentions().count();
const auto loadedCount = thread->unreadMentions().loadedCount();
const auto allLoaded = (fullCount >= 0) && (loadedCount >= fullCount);
if (fullCount >= 0 && loadedCount < kPreloadIfLess && !allLoaded) {
requestMentions(thread, loadedCount);
}
}
void UnreadThings::preloadEnoughReactions(not_null<Data::Thread*> thread) {
const auto fullCount = thread->unreadReactions().count();
const auto loadedCount = thread->unreadReactions().loadedCount();
const auto allLoaded = (fullCount >= 0) && (loadedCount >= fullCount);
if (fullCount >= 0 && loadedCount < kPreloadIfLess && !allLoaded) {
requestReactions(thread, loadedCount);
}
}
void UnreadThings::cancelRequests(not_null<Data::Thread*> thread) {
if (const auto requestId = _mentionsRequests.take(thread)) {
_api->request(*requestId).cancel();
}
if (const auto requestId = _reactionsRequests.take(thread)) {
_api->request(*requestId).cancel();
}
}
void UnreadThings::requestMentions(
not_null<Data::Thread*> thread,
int loaded) {
if (_mentionsRequests.contains(thread) || thread->asSublist()) {
return;
}
const auto offsetId = std::max(
thread->unreadMentions().maxLoaded(),
MsgId(1));
const auto limit = loaded ? kNextRequestLimit : kFirstRequestLimit;
const auto addOffset = loaded ? -(limit + 1) : -limit;
const auto maxId = 0;
const auto minId = 0;
const auto history = thread->owningHistory();
const auto topic = thread->asTopic();
using Flag = MTPmessages_GetUnreadMentions::Flag;
const auto requestId = _api->request(MTPmessages_GetUnreadMentions(
MTP_flags(topic ? Flag::f_top_msg_id : Flag()),
history->peer->input(),
MTP_int(topic ? topic->rootId() : 0),
MTP_int(offsetId),
MTP_int(addOffset),
MTP_int(limit),
MTP_int(maxId),
MTP_int(minId)
)).done([=](const MTPmessages_Messages &result) {
_mentionsRequests.remove(thread);
thread->unreadMentions().addSlice(result, loaded);
}).fail([=] {
_mentionsRequests.remove(thread);
}).send();
_mentionsRequests.emplace(thread, requestId);
}
void UnreadThings::requestReactions(
not_null<Data::Thread*> thread,
int loaded) {
if (_reactionsRequests.contains(thread)) {
return;
}
const auto offsetId = loaded
? std::max(thread->unreadReactions().maxLoaded(), MsgId(1))
: MsgId(1);
const auto limit = loaded ? kNextRequestLimit : kFirstRequestLimit;
const auto addOffset = loaded ? -(limit + 1) : -limit;
const auto maxId = 0;
const auto minId = 0;
const auto history = thread->owningHistory();
const auto sublist = thread->asSublist();
const auto topic = thread->asTopic();
using Flag = MTPmessages_GetUnreadReactions::Flag;
const auto requestId = _api->request(MTPmessages_GetUnreadReactions(
MTP_flags((topic ? Flag::f_top_msg_id : Flag())
| (sublist ? Flag::f_saved_peer_id : Flag())),
history->peer->input(),
MTP_int(topic ? topic->rootId() : 0),
(sublist ? sublist->sublistPeer()->input() : MTPInputPeer()),
MTP_int(offsetId),
MTP_int(addOffset),
MTP_int(limit),
MTP_int(maxId),
MTP_int(minId)
)).done([=](const MTPmessages_Messages &result) {
_reactionsRequests.remove(thread);
thread->unreadReactions().addSlice(result, loaded);
}).fail([=] {
_reactionsRequests.remove(thread);
}).send();
_reactionsRequests.emplace(thread, requestId);
}
} // namespace UnreadThings

View File

@@ -0,0 +1,49 @@
/*
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
*/
#pragma once
class ApiWrap;
class PeerData;
class ChannelData;
namespace Data {
class Thread;
} // namespace Data
namespace Api {
class UnreadThings final {
public:
explicit UnreadThings(not_null<ApiWrap*> api);
[[nodiscard]] bool trackMentions(Data::Thread *thread) const;
[[nodiscard]] bool trackReactions(Data::Thread *thread) const;
void preloadEnough(Data::Thread *thread);
void mediaAndMentionsRead(
const base::flat_set<MsgId> &readIds,
ChannelData *channel = nullptr);
void cancelRequests(not_null<Data::Thread*> thread);
private:
void preloadEnoughMentions(not_null<Data::Thread*> thread);
void preloadEnoughReactions(not_null<Data::Thread*> thread);
void requestMentions(not_null<Data::Thread*> thread, int loaded);
void requestReactions(not_null<Data::Thread*> thread, int loaded);
const not_null<ApiWrap*> _api;
base::flat_map<not_null<Data::Thread*>, mtpRequestId> _mentionsRequests;
base::flat_map<not_null<Data::Thread*>, mtpRequestId> _reactionsRequests;
};
} // namespace Api

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,223 @@
/*
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
*/
#pragma once
#include "data/data_pts_waiter.h"
#include "base/timer.h"
class ApiWrap;
class History;
namespace MTP {
class Error;
} // namespace MTP
namespace Main {
class Session;
} // namespace Main
namespace ChatHelpers {
struct EmojiInteractionsBunch;
} // namespace ChatHelpers
namespace Api {
class Updates final {
public:
explicit Updates(not_null<Main::Session*> session);
[[nodiscard]] Main::Session &session() const;
[[nodiscard]] ApiWrap &api() const;
void applyUpdates(
const MTPUpdates &updates,
uint64 sentMessageRandomId = 0);
void applyUpdatesNoPtsCheck(const MTPUpdates &updates);
void applyUpdateNoPtsCheck(const MTPUpdate &update);
void checkForSentToScheduled(const MTPUpdates &updates);
[[nodiscard]] int32 pts() const;
void updateOnline(crl::time lastNonIdleTime = 0);
[[nodiscard]] bool isIdle() const;
[[nodiscard]] rpl::producer<bool> isIdleValue() const;
void checkIdleFinish(crl::time lastNonIdleTime = 0);
bool lastWasOnline() const;
crl::time lastSetOnline() const;
bool isQuitPrevent();
bool updateAndApply(int32 pts, int32 ptsCount, const MTPUpdates &updates);
bool updateAndApply(int32 pts, int32 ptsCount, const MTPUpdate &update);
bool updateAndApply(int32 pts, int32 ptsCount);
void checkLastUpdate(bool afterSleep);
// ms <= 0 - stop timer
void ptsWaiterStartTimerFor(ChannelData *channel, crl::time ms);
void getDifference();
void requestChannelRangeDifference(not_null<History*> history);
void addActiveChat(rpl::producer<PeerData*> chat);
[[nodiscard]] bool inActiveChats(not_null<PeerData*> peer) const;
private:
enum class ChannelDifferenceRequest {
Unknown,
PtsGapOrShortPoll,
AfterFail,
};
enum class SkipUpdatePolicy {
SkipNone,
SkipMessageIds,
SkipExceptGroupCallParticipants,
};
struct ActiveChatTracker {
PeerData *peer = nullptr;
rpl::lifetime lifetime;
};
void channelRangeDifferenceSend(
not_null<ChannelData*> channel,
MsgRange range,
int32 pts);
void channelRangeDifferenceDone(
not_null<ChannelData*> channel,
MsgRange range,
const MTPupdates_ChannelDifference &result);
void updateOnline(crl::time lastNonIdleTime, bool gotOtherOffline);
void sendPing();
void getDifferenceByPts();
void getDifferenceAfterFail();
[[nodiscard]] bool requestingDifference() const {
return _ptsWaiter.requesting();
}
void getChannelDifference(
not_null<ChannelData*> channel,
ChannelDifferenceRequest from = ChannelDifferenceRequest::Unknown);
void differenceDone(const MTPupdates_Difference &result);
void differenceFail(const MTP::Error &error);
void feedDifference(
const MTPVector<MTPUser> &users,
const MTPVector<MTPChat> &chats,
const MTPVector<MTPMessage> &msgs,
const MTPVector<MTPUpdate> &other);
void stateDone(const MTPupdates_State &state);
void setState(int32 pts, int32 date, int32 qts, int32 seq);
void channelDifferenceDone(
not_null<ChannelData*> channel,
const MTPupdates_ChannelDifference &diff);
void channelDifferenceFail(
not_null<ChannelData*> channel,
const MTP::Error &error);
void failDifferenceStartTimerFor(ChannelData *channel);
void feedChannelDifference(const MTPDupdates_channelDifference &data);
void mtpUpdateReceived(const MTPUpdates &updates);
void mtpNewSessionCreated();
void feedUpdateVector(
const MTPVector<MTPUpdate> &updates,
SkipUpdatePolicy policy = SkipUpdatePolicy::SkipNone);
// Doesn't call sendHistoryChangeNotifications itself.
void feedMessageIds(const MTPVector<MTPUpdate> &updates);
// Doesn't call sendHistoryChangeNotifications itself.
void feedUpdate(const MTPUpdate &update);
void applyConvertToScheduledOnSend(
const MTPVector<MTPUpdate> &other,
bool skipScheduledCheck = false);
void applyGroupCallParticipantUpdates(const MTPUpdates &updates);
bool whenGetDiffChanged(
ChannelData *channel,
int32 ms,
base::flat_map<not_null<ChannelData*>, crl::time> &whenMap,
crl::time &curTime);
void handleSendActionUpdate(
PeerId peerId,
MsgId rootId,
PeerId fromId,
const MTPSendMessageAction &action);
void handleEmojiInteraction(
not_null<PeerData*> peer,
const MTPDsendMessageEmojiInteraction &data);
void handleSpeakingInCall(
not_null<PeerData*> peer,
PeerId participantPeerId,
PeerData *participantPeerLoaded);
void handleEmojiInteraction(
not_null<PeerData*> peer,
MsgId messageId,
const QString &emoticon,
ChatHelpers::EmojiInteractionsBunch bunch);
void handleEmojiInteraction(
not_null<PeerData*> peer,
const QString &emoticon);
const not_null<Main::Session*> _session;
int32 _updatesDate = 0;
int32 _updatesQts = -1;
int32 _updatesSeq = 0;
base::Timer _noUpdatesTimer;
base::Timer _onlineTimer;
PtsWaiter _ptsWaiter;
base::flat_map<not_null<ChannelData*>, crl::time> _whenGetDiffByPts;
base::flat_map<not_null<ChannelData*>, crl::time> _whenGetDiffAfterFail;
crl::time _getDifferenceTimeByPts = 0;
crl::time _getDifferenceTimeAfterFail = 0;
base::Timer _byPtsTimer;
base::flat_map<int32, MTPUpdates> _bySeqUpdates;
base::Timer _bySeqTimer;
base::Timer _byMinChannelTimer;
// growing timeout for getDifference calls, if it fails
crl::time _failDifferenceTimeout = 1;
// growing timeout for getChannelDifference calls, if it fails
base::flat_map<
not_null<ChannelData*>,
crl::time> _channelFailDifferenceTimeout;
base::Timer _failDifferenceTimer;
base::flat_map<
not_null<ChannelData*>,
mtpRequestId> _rangeDifferenceRequests;
crl::time _lastUpdateTime = 0;
bool _handlingChannelDifference = false;
base::flat_map<int, ActiveChatTracker> _activeChats;
base::flat_map<
not_null<PeerData*>,
base::flat_map<PeerId, crl::time>> _pendingSpeakingCallParticipants;
mtpRequestId _onlineRequest = 0;
base::Timer _idleFinishTimer;
crl::time _lastSetOnline = 0;
bool _lastWasOnline = false;
rpl::variable<bool> _isIdle = false;
rpl::lifetime _lifetime;
};
[[nodiscard]] bool IsWithdrawalNotification(
const MTPDupdateServiceNotification &);
} // namespace Api

View File

@@ -0,0 +1,264 @@
/*
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 "api/api_user_names.h"
#include "apiwrap.h"
#include "data/data_channel.h"
#include "data/data_peer.h"
#include "data/data_user.h"
#include "main/main_session.h"
namespace Api {
namespace {
[[nodiscard]] Data::Username UsernameFromTL(const MTPUsername &username) {
return {
.username = qs(username.data().vusername()),
.active = username.data().is_active(),
.editable = username.data().is_editable(),
};
}
[[nodiscard]] std::optional<MTPInputUser> BotUserInput(
not_null<PeerData*> peer) {
const auto user = peer->asUser();
return (user && user->botInfo && user->botInfo->canEditInformation)
? std::make_optional<MTPInputUser>(user->inputUser())
: std::nullopt;
}
} // namespace
Usernames::Usernames(not_null<ApiWrap*> api)
: _session(&api->session())
, _api(&api->instance()) {
}
rpl::producer<Data::Usernames> Usernames::loadUsernames(
not_null<PeerData*> peer) const {
return [=](auto consumer) {
auto lifetime = rpl::lifetime();
const auto push = [consumer](
const auto &usernames,
const auto &username) {
if (usernames) {
if (usernames->v.empty()) {
// Probably will never happen.
consumer.put_next({});
} else {
auto parsed = FromTL(*usernames);
if ((parsed.size() == 1)
&& username
&& (parsed.front().username == qs(*username))) {
// Probably will never happen.
consumer.put_next({});
} else {
consumer.put_next(std::move(parsed));
}
}
} else {
consumer.put_next({});
}
};
const auto requestUser = [&](const MTPInputUser &data) {
_session->api().request(MTPusers_GetUsers(
MTP_vector<MTPInputUser>(1, data)
)).done([=](const MTPVector<MTPUser> &result) {
result.v.front().match([&](const MTPDuser &data) {
push(data.vusernames(), data.vusername());
consumer.put_done();
}, [&](const MTPDuserEmpty&) {
consumer.put_next({});
consumer.put_done();
});
}).send();
};
const auto requestChannel = [&](const MTPInputChannel &data) {
_session->api().request(MTPchannels_GetChannels(
MTP_vector<MTPInputChannel>(1, data)
)).done([=](const MTPmessages_Chats &result) {
result.match([&](const auto &data) {
data.vchats().v.front().match([&](const MTPDchannel &c) {
push(c.vusernames(), c.vusername());
consumer.put_done();
}, [&](auto &&) {
consumer.put_next({});
consumer.put_done();
});
});
}).send();
};
if (peer->isSelf()) {
requestUser(MTP_inputUserSelf());
} else if (const auto user = peer->asUser()) {
requestUser(user->inputUser());
} else if (const auto channel = peer->asChannel()) {
requestChannel(channel->inputChannel());
}
return lifetime;
};
}
rpl::producer<rpl::no_value, Usernames::Error> Usernames::toggle(
not_null<PeerData*> peer,
const QString &username,
bool active) {
const auto peerId = peer->id;
const auto it = _toggleRequests.find(peerId);
const auto found = (it != end(_toggleRequests));
auto &entry = (!found
? _toggleRequests.emplace(
peerId,
Entry{ .usernames = { username } }).first
: it)->second;
if (ranges::contains(entry.usernames, username)) {
if (found) {
return entry.done.events();
}
} else {
entry.usernames.push_back(username);
}
const auto pop = [=](Error error) {
const auto it = _toggleRequests.find(peerId);
if (it != end(_toggleRequests)) {
auto &list = it->second.usernames;
list.erase(ranges::remove(list, username), end(list));
if (list.empty()) {
if (error == Error::Unknown) {
it->second.done.fire_done();
} else {
it->second.done.fire_error_copy(error);
}
_toggleRequests.remove(peerId);
}
}
};
const auto done = [=] {
pop(Error::Unknown);
};
const auto fail = [=](const MTP::Error &error) {
const auto type = error.type();
if (type == u"USERNAMES_ACTIVE_TOO_MUCH"_q) {
pop(Error::TooMuch);
} else if (type.startsWith(u"FLOOD_WAIT_"_q)) {
pop(Error::Flood);
} else {
pop(Error::Unknown);
}
};
if (peer->isSelf()) {
_api.request(MTPaccount_ToggleUsername(
MTP_string(username),
MTP_bool(active)
)).done(done).fail(fail).handleFloodErrors().send();
} else if (const auto channel = peer->asChannel()) {
_api.request(MTPchannels_ToggleUsername(
channel->inputChannel(),
MTP_string(username),
MTP_bool(active)
)).done(done).fail(fail).handleFloodErrors().send();
} else if (const auto botUserInput = BotUserInput(peer)) {
_api.request(MTPbots_ToggleUsername(
*botUserInput,
MTP_string(username),
MTP_bool(active)
)).done(done).fail(fail).handleFloodErrors().send();
} else {
return rpl::never<rpl::no_value, Error>();
}
return entry.done.events();
}
rpl::producer<> Usernames::reorder(
not_null<PeerData*> peer,
const std::vector<QString> &usernames) {
const auto peerId = peer->id;
const auto it = _reorderRequests.find(peerId);
if (it != end(_reorderRequests)) {
_api.request(it->second).cancel();
_reorderRequests.erase(peerId);
}
return [=](auto consumer) {
auto lifetime = rpl::lifetime();
auto tlUsernames = ranges::views::all(
usernames
) | ranges::views::transform([](const QString &username) {
return MTP_string(username);
}) | ranges::to<QVector<MTPstring>>;
const auto finish = [=] {
if (_reorderRequests.contains(peerId)) {
_reorderRequests.erase(peerId);
}
consumer.put_done();
};
if (usernames.empty()) {
crl::on_main([=] { consumer.put_done(); });
return lifetime;
}
if (peer->isSelf()) {
const auto requestId = _api.request(MTPaccount_ReorderUsernames(
MTP_vector<MTPstring>(std::move(tlUsernames))
)).done(finish).fail(finish).send();
_reorderRequests.emplace(peerId, requestId);
} else if (const auto channel = peer->asChannel()) {
const auto requestId = _api.request(MTPchannels_ReorderUsernames(
channel->inputChannel(),
MTP_vector<MTPstring>(std::move(tlUsernames))
)).done(finish).fail(finish).send();
_reorderRequests.emplace(peerId, requestId);
} else if (const auto botUserInput = BotUserInput(peer)) {
const auto requestId = _api.request(MTPbots_ReorderUsernames(
*botUserInput,
MTP_vector<MTPstring>(std::move(tlUsernames))
)).done(finish).fail(finish).send();
_reorderRequests.emplace(peerId, requestId);
}
return lifetime;
};
}
Data::Usernames Usernames::FromTL(const MTPVector<MTPUsername> &usernames) {
return ranges::views::all(
usernames.v
) | ranges::views::transform(UsernameFromTL) | ranges::to_vector;
}
void Usernames::requestToCache(not_null<PeerData*> peer) {
_tinyCache = {};
if (const auto user = peer->asUser()) {
if (user->usernames().empty()) {
return;
}
} else if (const auto channel = peer->asChannel()) {
if (channel->usernames().empty()) {
return;
}
}
const auto lifetime = std::make_shared<rpl::lifetime>();
*lifetime = loadUsernames(
peer
) | rpl::on_next([=, id = peer->id](Data::Usernames usernames) {
_tinyCache = std::make_pair(id, std::move(usernames));
lifetime->destroy();
});
}
Data::Usernames Usernames::cacheFor(PeerId id) {
return (_tinyCache.first == id) ? _tinyCache.second : Data::Usernames();
}
} // namespace Api

View File

@@ -0,0 +1,64 @@
/*
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
*/
#pragma once
#include "data/data_user_names.h"
#include "mtproto/sender.h"
class ApiWrap;
class PeerData;
namespace Main {
class Session;
} // namespace Main
namespace Api {
class Usernames final {
public:
enum class Error {
TooMuch,
Flood,
Unknown,
};
explicit Usernames(not_null<ApiWrap*> api);
[[nodiscard]] rpl::producer<Data::Usernames> loadUsernames(
not_null<PeerData*> peer) const;
[[nodiscard]] rpl::producer<rpl::no_value, Error> toggle(
not_null<PeerData*> peer,
const QString &username,
bool active);
[[nodiscard]] rpl::producer<> reorder(
not_null<PeerData*> peer,
const std::vector<QString> &usernames);
void requestToCache(not_null<PeerData*> peer);
[[nodiscard]] Data::Usernames cacheFor(PeerId id);
static Data::Usernames FromTL(const MTPVector<MTPUsername> &usernames);
private:
const not_null<Main::Session*> _session;
MTP::Sender _api;
using Key = PeerId;
struct Entry final {
rpl::event_stream<rpl::no_value, Error> done;
std::vector<QString> usernames;
};
base::flat_map<Key, Entry> _toggleRequests;
base::flat_map<Key, mtpRequestId> _reorderRequests;
// Used for a seamless display of usernames list.
std::pair<Key, Data::Usernames> _tinyCache;
};
} // namespace Api

View File

@@ -0,0 +1,345 @@
/*
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 "api/api_user_privacy.h"
#include "apiwrap.h"
#include "data/data_chat.h"
#include "data/data_channel.h"
#include "data/data_peer.h"
#include "data/data_peer_id.h"
#include "data/data_session.h"
#include "data/data_user.h"
#include "main/main_session.h"
#include "settings/settings_premium.h" // Settings::ShowPremium.
namespace Api {
namespace {
constexpr auto kMaxRules = 3; // Allow users, disallow users, Option.
using TLInputRules = MTPVector<MTPInputPrivacyRule>;
using TLRules = MTPVector<MTPPrivacyRule>;
TLInputRules RulesToTL(const UserPrivacy::Rule &rule) {
using Exceptions = UserPrivacy::Exceptions;
const auto collectInputUsers = [](const Exceptions &exceptions) {
const auto &peers = exceptions.peers;
auto result = QVector<MTPInputUser>();
result.reserve(peers.size());
for (const auto &peer : peers) {
if (const auto user = peer->asUser()) {
result.push_back(user->inputUser());
}
}
return result;
};
const auto collectInputChats = [](const Exceptions &exceptions) {
const auto &peers = exceptions.peers;
auto result = QVector<MTPlong>();
result.reserve(peers.size());
for (const auto &peer : peers) {
if (!peer->isUser()) {
result.push_back(peerToBareMTPInt(peer->id));
}
}
return result;
};
using Option = UserPrivacy::Option;
auto result = QVector<MTPInputPrivacyRule>();
result.reserve(kMaxRules);
if (!rule.ignoreAlways) {
const auto users = collectInputUsers(rule.always);
const auto chats = collectInputChats(rule.always);
if (!users.empty()) {
result.push_back(
MTP_inputPrivacyValueAllowUsers(
MTP_vector<MTPInputUser>(users)));
}
if (!chats.empty()) {
result.push_back(
MTP_inputPrivacyValueAllowChatParticipants(
MTP_vector<MTPlong>(chats)));
}
if (rule.always.premiums && (rule.option != Option::Everyone)) {
result.push_back(MTP_inputPrivacyValueAllowPremium());
}
if (rule.always.miniapps && (rule.option != Option::Everyone)) {
result.push_back(MTP_inputPrivacyValueAllowBots());
}
}
if (!rule.ignoreNever) {
const auto users = collectInputUsers(rule.never);
const auto chats = collectInputChats(rule.never);
if (!users.empty()) {
result.push_back(
MTP_inputPrivacyValueDisallowUsers(
MTP_vector<MTPInputUser>(users)));
}
if (!chats.empty()) {
result.push_back(
MTP_inputPrivacyValueDisallowChatParticipants(
MTP_vector<MTPlong>(chats)));
}
if (rule.never.miniapps && (rule.option != Option::Nobody)) {
result.push_back(MTP_inputPrivacyValueDisallowBots());
}
}
result.push_back([&] {
switch (rule.option) {
case Option::Everyone: return MTP_inputPrivacyValueAllowAll();
case Option::Contacts: return MTP_inputPrivacyValueAllowContacts();
case Option::CloseFriends:
return MTP_inputPrivacyValueAllowCloseFriends();
case Option::Nobody: return MTP_inputPrivacyValueDisallowAll();
}
Unexpected("Option value in Api::UserPrivacy::RulesToTL.");
}());
return MTP_vector<MTPInputPrivacyRule>(std::move(result));
}
UserPrivacy::Rule TLToRules(const TLRules &rules, Data::Session &owner) {
// This is simplified version of privacy rules interpretation.
// But it should be fine for all the apps
// that use the same subset of features.
using Option = UserPrivacy::Option;
auto result = UserPrivacy::Rule();
auto optionSet = false;
const auto setOption = [&](Option option) {
if (optionSet) {
return;
}
optionSet = true;
result.option = option;
};
auto &always = result.always.peers;
auto &never = result.never.peers;
const auto feed = [&](const MTPPrivacyRule &rule) {
rule.match([&](const MTPDprivacyValueAllowAll &) {
setOption(Option::Everyone);
}, [&](const MTPDprivacyValueAllowContacts &) {
setOption(Option::Contacts);
}, [&](const MTPDprivacyValueAllowCloseFriends &) {
setOption(Option::CloseFriends);
}, [&](const MTPDprivacyValueAllowPremium &) {
result.always.premiums = true;
}, [&](const MTPDprivacyValueAllowBots &) {
result.always.miniapps = true;
}, [&](const MTPDprivacyValueDisallowBots &) {
result.never.miniapps = true;
}, [&](const MTPDprivacyValueAllowUsers &data) {
const auto &users = data.vusers().v;
always.reserve(always.size() + users.size());
for (const auto &userId : users) {
const auto user = owner.user(UserId(userId.v));
if (!base::contains(never, user)
&& !base::contains(always, user)) {
always.emplace_back(user);
}
}
}, [&](const MTPDprivacyValueAllowChatParticipants &data) {
const auto &chats = data.vchats().v;
always.reserve(always.size() + chats.size());
for (const auto &chatId : chats) {
const auto chat = owner.chatLoaded(chatId);
const auto peer = chat
? static_cast<PeerData*>(chat)
: owner.channelLoaded(chatId);
if (peer
&& !base::contains(never, peer)
&& !base::contains(always, peer)) {
always.emplace_back(peer);
}
}
}, [&](const MTPDprivacyValueDisallowContacts &) {
// Not supported
}, [&](const MTPDprivacyValueDisallowAll &) {
setOption(Option::Nobody);
}, [&](const MTPDprivacyValueDisallowUsers &data) {
const auto &users = data.vusers().v;
never.reserve(never.size() + users.size());
for (const auto &userId : users) {
const auto user = owner.user(UserId(userId.v));
if (!base::contains(always, user)
&& !base::contains(never, user)) {
never.emplace_back(user);
}
}
}, [&](const MTPDprivacyValueDisallowChatParticipants &data) {
const auto &chats = data.vchats().v;
never.reserve(never.size() + chats.size());
for (const auto &chatId : chats) {
const auto chat = owner.chatLoaded(chatId);
const auto peer = chat
? static_cast<PeerData*>(chat)
: owner.channelLoaded(chatId);
if (peer
&& !base::contains(always, peer)
&& !base::contains(never, peer)) {
never.emplace_back(peer);
}
}
});
};
for (const auto &rule : rules.v) {
feed(rule);
}
feed(MTP_privacyValueDisallowAll()); // Disallow by default.
return result;
}
MTPInputPrivacyKey KeyToTL(UserPrivacy::Key key) {
using Key = UserPrivacy::Key;
switch (key) {
case Key::Calls: return MTP_inputPrivacyKeyPhoneCall();
case Key::Invites: return MTP_inputPrivacyKeyChatInvite();
case Key::PhoneNumber: return MTP_inputPrivacyKeyPhoneNumber();
case Key::AddedByPhone: return MTP_inputPrivacyKeyAddedByPhone();
case Key::LastSeen: return MTP_inputPrivacyKeyStatusTimestamp();
case Key::CallsPeer2Peer: return MTP_inputPrivacyKeyPhoneP2P();
case Key::Forwards: return MTP_inputPrivacyKeyForwards();
case Key::ProfilePhoto: return MTP_inputPrivacyKeyProfilePhoto();
case Key::Voices: return MTP_inputPrivacyKeyVoiceMessages();
case Key::About: return MTP_inputPrivacyKeyAbout();
case Key::Birthday: return MTP_inputPrivacyKeyBirthday();
case Key::GiftsAutoSave: return MTP_inputPrivacyKeyStarGiftsAutoSave();
case Key::NoPaidMessages: return MTP_inputPrivacyKeyNoPaidMessages();
case Key::SavedMusic: return MTP_inputPrivacyKeySavedMusic();
}
Unexpected("Key in Api::UserPrivacy::KetToTL.");
}
std::optional<UserPrivacy::Key> TLToKey(mtpTypeId type) {
using Key = UserPrivacy::Key;
switch (type) {
case mtpc_privacyKeyPhoneNumber:
case mtpc_inputPrivacyKeyPhoneNumber: return Key::PhoneNumber;
case mtpc_privacyKeyAddedByPhone:
case mtpc_inputPrivacyKeyAddedByPhone: return Key::AddedByPhone;
case mtpc_privacyKeyStatusTimestamp:
case mtpc_inputPrivacyKeyStatusTimestamp: return Key::LastSeen;
case mtpc_privacyKeyChatInvite:
case mtpc_inputPrivacyKeyChatInvite: return Key::Invites;
case mtpc_privacyKeyPhoneCall:
case mtpc_inputPrivacyKeyPhoneCall: return Key::Calls;
case mtpc_privacyKeyPhoneP2P:
case mtpc_inputPrivacyKeyPhoneP2P: return Key::CallsPeer2Peer;
case mtpc_privacyKeyForwards:
case mtpc_inputPrivacyKeyForwards: return Key::Forwards;
case mtpc_privacyKeyProfilePhoto:
case mtpc_inputPrivacyKeyProfilePhoto: return Key::ProfilePhoto;
case mtpc_privacyKeyVoiceMessages:
case mtpc_inputPrivacyKeyVoiceMessages: return Key::Voices;
case mtpc_privacyKeyAbout:
case mtpc_inputPrivacyKeyAbout: return Key::About;
case mtpc_privacyKeyBirthday:
case mtpc_inputPrivacyKeyBirthday: return Key::Birthday;
case mtpc_privacyKeyStarGiftsAutoSave:
case mtpc_inputPrivacyKeyStarGiftsAutoSave: return Key::GiftsAutoSave;
case mtpc_privacyKeyNoPaidMessages:
case mtpc_inputPrivacyKeyNoPaidMessages: return Key::NoPaidMessages;
case mtpc_privacyKeySavedMusic:
case mtpc_inputPrivacyKeySavedMusic: return Key::SavedMusic;
}
return std::nullopt;
}
} // namespace
UserPrivacy::UserPrivacy(not_null<ApiWrap*> api)
: _session(&api->session())
, _api(&api->instance()) {
}
void UserPrivacy::save(
Key key,
const UserPrivacy::Rule &rule) {
const auto tlKey = KeyToTL(key);
const auto keyTypeId = tlKey.type();
const auto it = _privacySaveRequests.find(keyTypeId);
if (it != _privacySaveRequests.cend()) {
_api.request(it->second).cancel();
_privacySaveRequests.erase(it);
}
const auto requestId = _api.request(MTPaccount_SetPrivacy(
tlKey,
RulesToTL(rule)
)).done([=](const MTPaccount_PrivacyRules &result) {
result.match([&](const MTPDaccount_privacyRules &data) {
_session->data().processUsers(data.vusers());
_session->data().processChats(data.vchats());
_privacySaveRequests.remove(keyTypeId);
apply(keyTypeId, data.vrules(), true);
});
}).fail([=](const MTP::Error &error) {
const auto message = error.type();
if (message == u"PREMIUM_ACCOUNT_REQUIRED"_q) {
Settings::ShowPremium(_session, QString());
}
_privacySaveRequests.remove(keyTypeId);
}).send();
_privacySaveRequests.emplace(keyTypeId, requestId);
}
void UserPrivacy::apply(
mtpTypeId type,
const TLRules &rules,
bool allLoaded) {
if (const auto key = TLToKey(type)) {
if (!allLoaded) {
reload(*key);
return;
}
pushPrivacy(*key, rules);
if ((*key) == Key::LastSeen) {
_session->api().updatePrivacyLastSeens();
}
}
}
void UserPrivacy::reload(Key key) {
if (_privacyRequestIds.contains(key)) {
return;
}
const auto requestId = _api.request(MTPaccount_GetPrivacy(
KeyToTL(key)
)).done([=](const MTPaccount_PrivacyRules &result) {
_privacyRequestIds.erase(key);
result.match([&](const MTPDaccount_privacyRules &data) {
_session->data().processUsers(data.vusers());
_session->data().processChats(data.vchats());
pushPrivacy(key, data.vrules());
});
}).fail([=] {
_privacyRequestIds.erase(key);
}).send();
_privacyRequestIds.emplace(key, requestId);
}
void UserPrivacy::pushPrivacy(Key key, const TLRules &rules) {
const auto &saved
= (_privacyValues[key] = TLToRules(rules, _session->data()));
const auto i = _privacyChanges.find(key);
if (i != end(_privacyChanges)) {
i->second.fire_copy(saved);
}
}
auto UserPrivacy::value(Key key) -> rpl::producer<UserPrivacy::Rule> {
if (const auto i = _privacyValues.find(key); i != end(_privacyValues)) {
return _privacyChanges[key].events_starting_with_copy(i->second);
} else {
return _privacyChanges[key].events();
}
}
} // namespace Api

View File

@@ -0,0 +1,85 @@
/*
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
*/
#pragma once
#include "mtproto/sender.h"
class ApiWrap;
namespace Main {
class Session;
} // namespace Main
namespace Api {
class UserPrivacy final {
public:
enum class Key {
PhoneNumber,
AddedByPhone,
LastSeen,
Calls,
Invites,
CallsPeer2Peer,
Forwards,
ProfilePhoto,
Voices,
About,
Birthday,
GiftsAutoSave,
NoPaidMessages,
SavedMusic,
};
enum class Option {
Everyone,
Contacts,
CloseFriends,
Nobody,
};
struct Exceptions {
std::vector<not_null<PeerData*>> peers;
bool premiums = false;
bool miniapps = false;
};
struct Rule {
Option option = Option::Everyone;
Exceptions always;
Exceptions never;
bool ignoreAlways = false;
bool ignoreNever = false;
};
explicit UserPrivacy(not_null<ApiWrap*> api);
void save(
Key key,
const UserPrivacy::Rule &rule);
void apply(
mtpTypeId type,
const MTPVector<MTPPrivacyRule> &rules,
bool allLoaded);
void reload(Key key);
rpl::producer<Rule> value(Key key);
private:
const not_null<Main::Session*> _session;
void pushPrivacy(Key key, const MTPVector<MTPPrivacyRule> &rules);
base::flat_map<mtpTypeId, mtpRequestId> _privacySaveRequests;
base::flat_map<Key, mtpRequestId> _privacyRequestIds;
base::flat_map<Key, Rule> _privacyValues;
std::map<Key, rpl::event_stream<Rule>> _privacyChanges;
MTP::Sender _api;
};
} // namespace Api

View File

@@ -0,0 +1,252 @@
/*
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 "api/api_views.h"
#include "apiwrap.h"
#include "data/data_peer.h"
#include "data/data_peer_id.h"
#include "data/data_session.h"
#include "history/history.h"
#include "history/history_item.h"
#include "main/main_session.h"
namespace Api {
namespace {
// Send channel views each second.
constexpr auto kSendViewsTimeout = crl::time(1000);
constexpr auto kPollExtendedMediaPeriod = 30 * crl::time(1000);
constexpr auto kMaxPollPerRequest = 100;
} // namespace
ViewsManager::ViewsManager(not_null<ApiWrap*> api)
: _session(&api->session())
, _api(&api->instance())
, _incrementTimer([=] { viewsIncrement(); })
, _pollTimer([=] { sendPollRequests(); }) {
}
void ViewsManager::scheduleIncrement(not_null<HistoryItem*> item) {
auto peer = item->history()->peer;
auto i = _incremented.find(peer);
if (i != _incremented.cend()) {
if (i->second.contains(item->id)) {
return;
}
} else {
i = _incremented.emplace(peer).first;
}
i->second.emplace(item->id);
auto j = _toIncrement.find(peer);
if (j == _toIncrement.cend()) {
j = _toIncrement.emplace(peer).first;
_incrementTimer.callOnce(kSendViewsTimeout);
}
j->second.emplace(item->id);
}
void ViewsManager::removeIncremented(not_null<PeerData*> peer) {
_incremented.remove(peer);
}
void ViewsManager::pollExtendedMedia(
not_null<HistoryItem*> item,
bool force) {
if (!item->isRegular()) {
return;
}
const auto id = item->id;
const auto peer = item->history()->peer;
auto &request = _pollRequests[peer];
if (request.ids.contains(id) || request.sent.contains(id)) {
if (!force || request.forced) {
return;
}
}
request.ids.emplace(id);
if (force) {
request.forced = true;
}
const auto delay = force ? 1 : kPollExtendedMediaPeriod;
if (!request.id && (!request.when || force)) {
request.when = crl::now() + delay;
}
if (!_pollTimer.isActive() || force) {
_pollTimer.callOnce(delay);
}
}
void ViewsManager::viewsIncrement() {
for (auto i = _toIncrement.begin(); i != _toIncrement.cend();) {
if (_incrementRequests.contains(i->first)) {
++i;
continue;
}
QVector<MTPint> ids;
ids.reserve(i->second.size());
for (const auto &msgId : i->second) {
ids.push_back(MTP_int(msgId));
}
const auto requestId = _api.request(MTPmessages_GetMessagesViews(
i->first->input(),
MTP_vector<MTPint>(ids),
MTP_bool(true)
)).done([=](
const MTPmessages_MessageViews &result,
mtpRequestId requestId) {
done(ids, result, requestId);
}).fail([=](const MTP::Error &error, mtpRequestId requestId) {
fail(error, requestId);
}).afterDelay(5).send();
_incrementRequests.emplace(i->first, requestId);
i = _toIncrement.erase(i);
}
}
void ViewsManager::sendPollRequests() {
const auto now = crl::now();
auto toRequest = base::flat_map<not_null<PeerData*>, QVector<MTPint>>();
auto nearest = crl::time();
for (auto &[peer, request] : _pollRequests) {
if (request.id) {
continue;
} else if (request.when <= now) {
Assert(request.sent.empty());
auto &list = toRequest[peer];
const auto count = int(request.ids.size());
if (count < kMaxPollPerRequest) {
request.sent = base::take(request.ids);
} else {
const auto from = begin(request.ids);
const auto end = from + kMaxPollPerRequest;
request.sent = { from, end };
request.ids.erase(from, end);
}
list.reserve(request.sent.size());
for (const auto &id : request.sent) {
list.push_back(MTP_int(id.bare));
}
if (!request.ids.empty()) {
nearest = now;
}
} else if (!nearest || nearest > request.when) {
nearest = request.when;
}
}
sendPollRequests(toRequest);
if (nearest) {
_pollTimer.callOnce(std::max(nearest - now, crl::time(1)));
}
}
void ViewsManager::sendPollRequests(
const base::flat_map<
not_null<PeerData*>,
QVector<MTPint>> &batched) {
for (auto &[peer, list] : batched) {
const auto finish = [=, list = list](mtpRequestId id) {
const auto now = crl::now();
const auto owner = &_session->data();
for (auto i = begin(_pollRequests); i != end(_pollRequests);) {
if (i->second.id == id) {
const auto peer = i->first->id;
for (const auto &itemId : i->second.sent) {
if (const auto item = owner->message(peer, itemId)) {
owner->requestItemRepaint(item);
}
}
i->second.sent.clear();
i->second.id = 0;
if (i->second.ids.empty()) {
i = _pollRequests.erase(i);
} else {
const auto delay = i->second.forced
? 1
: kPollExtendedMediaPeriod;
i->second.when = now + delay;
if (!_pollTimer.isActive() || i->second.forced) {
_pollTimer.callOnce(delay);
}
++i;
}
} else {
++i;
}
}
};
const auto requestId = _api.request(MTPmessages_GetExtendedMedia(
peer->input(),
MTP_vector<MTPint>(list)
)).done([=](const MTPUpdates &result, mtpRequestId id) {
_session->api().applyUpdates(result);
finish(id);
}).fail([=](const MTP::Error &error, mtpRequestId id) {
finish(id);
}).send();
_pollRequests[peer].id = requestId;
}
}
void ViewsManager::done(
QVector<MTPint> ids,
const MTPmessages_MessageViews &result,
mtpRequestId requestId) {
const auto &data = result.c_messages_messageViews();
auto &owner = _session->data();
owner.processUsers(data.vusers());
owner.processChats(data.vchats());
auto &v = data.vviews().v;
if (ids.size() == v.size()) {
for (const auto &[peer, id] : _incrementRequests) {
if (id != requestId) {
continue;
}
for (auto j = 0, l = int(ids.size()); j < l; ++j) {
if (const auto item = owner.message(peer->id, ids[j].v)) {
v[j].match([&](const MTPDmessageViews &data) {
if (const auto views = data.vviews()) {
if (item->changeViewsCount(views->v)) {
_session->data().notifyItemDataChange(item);
}
}
if (const auto forwards = data.vforwards()) {
item->setForwardsCount(forwards->v);
}
if (const auto replies = data.vreplies()) {
item->setReplies(
HistoryMessageRepliesData(replies));
}
});
}
}
_incrementRequests.erase(peer);
break;
}
}
if (!_toIncrement.empty() && !_incrementTimer.isActive()) {
_incrementTimer.callOnce(kSendViewsTimeout);
}
}
void ViewsManager::fail(const MTP::Error &error, mtpRequestId requestId) {
for (const auto &[peer, id] : _incrementRequests) {
if (id == requestId) {
_incrementRequests.erase(peer);
break;
}
}
if (!_toIncrement.empty() && !_incrementTimer.isActive()) {
_incrementTimer.callOnce(kSendViewsTimeout);
}
}
} // namespace Api

View File

@@ -0,0 +1,69 @@
/*
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
*/
#pragma once
#include "mtproto/sender.h"
#include "base/timer.h"
class ApiWrap;
class PeerData;
namespace Main {
class Session;
} // namespace Main
namespace Api {
class ViewsManager final {
public:
explicit ViewsManager(not_null<ApiWrap*> api);
void scheduleIncrement(not_null<HistoryItem*> item);
void removeIncremented(not_null<PeerData*> peer);
void pollExtendedMedia(not_null<HistoryItem*> item, bool force = false);
private:
struct PollExtendedMediaRequest {
crl::time when = 0;
mtpRequestId id = 0;
base::flat_set<MsgId> ids;
base::flat_set<MsgId> sent;
bool forced = false;
};
void viewsIncrement();
void sendPollRequests();
void sendPollRequests(
const base::flat_map<
not_null<PeerData*>,
QVector<MTPint>> &prepared);
void done(
QVector<MTPint> ids,
const MTPmessages_MessageViews &result,
mtpRequestId requestId);
void fail(const MTP::Error &error, mtpRequestId requestId);
const not_null<Main::Session*> _session;
MTP::Sender _api;
base::flat_map<not_null<PeerData*>, base::flat_set<MsgId>> _incremented;
base::flat_map<not_null<PeerData*>, base::flat_set<MsgId>> _toIncrement;
base::flat_map<not_null<PeerData*>, mtpRequestId> _incrementRequests;
base::flat_map<mtpRequestId, not_null<PeerData*>> _incrementByRequest;
base::Timer _incrementTimer;
base::flat_map<
not_null<PeerData*>,
PollExtendedMediaRequest> _pollRequests;
base::Timer _pollTimer;
};
} // namespace Api

View File

@@ -0,0 +1,134 @@
/*
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 "api/api_websites.h"
#include "api/api_authorizations.h"
#include "api/api_blocked_peers.h"
#include "apiwrap.h"
#include "data/data_session.h"
#include "data/data_user.h"
#include "main/main_session.h"
namespace Api {
namespace {
[[nodiscard]] Websites::Entry ParseEntry(
not_null<Data::Session*> owner,
const MTPDwebAuthorization &data) {
auto result = Websites::Entry{
.hash = data.vhash().v,
.bot = owner->user(data.vbot_id()),
.platform = qs(data.vplatform()),
.domain = qs(data.vdomain()),
.browser = qs(data.vbrowser()),
.ip = qs(data.vip()),
.location = qs(data.vregion()),
};
result.activeTime = data.vdate_active().v
? data.vdate_active().v
: data.vdate_created().v;
result.active = Authorizations::ActiveDateString(result.activeTime);
return result;
}
} // namespace
Websites::Websites(not_null<ApiWrap*> api)
: _session(&api->session())
, _api(&api->instance()) {
}
void Websites::reload() {
if (_requestId) {
return;
}
_requestId = _api.request(MTPaccount_GetWebAuthorizations(
)).done([=](const MTPaccount_WebAuthorizations &result) {
_requestId = 0;
_lastReceived = crl::now();
const auto owner = &_session->data();
const auto &data = result.data();
owner->processUsers(data.vusers());
_list = ranges::views::all(
data.vauthorizations().v
) | ranges::views::transform([&](const MTPwebAuthorization &auth) {
return ParseEntry(owner, auth.data());
}) | ranges::to<List>;
_listChanges.fire({});
}).fail([=] {
_requestId = 0;
}).send();
}
void Websites::cancelCurrentRequest() {
_api.request(base::take(_requestId)).cancel();
}
void Websites::requestTerminate(
Fn<void(const MTPBool &result)> &&done,
Fn<void(const MTP::Error &error)> &&fail,
std::optional<uint64> hash,
UserData *botToBlock) {
const auto send = [&](auto request) {
_api.request(
std::move(request)
).done([=, done = std::move(done)](const MTPBool &result) {
done(result);
if (hash) {
_list.erase(
ranges::remove(_list, *hash, &Entry::hash),
end(_list));
} else {
_list.clear();
}
_listChanges.fire({});
}).fail(
std::move(fail)
).send();
};
if (hash) {
send(MTPaccount_ResetWebAuthorization(MTP_long(*hash)));
if (botToBlock) {
botToBlock->session().api().blockedPeers().block(botToBlock);
}
} else {
send(MTPaccount_ResetWebAuthorizations());
}
}
Websites::List Websites::list() const {
return _list;
}
auto Websites::listValue() const
-> rpl::producer<Websites::List> {
return rpl::single(
list()
) | rpl::then(
_listChanges.events() | rpl::map([=] { return list(); })
);
}
rpl::producer<int> Websites::totalValue() const {
return rpl::single(
total()
) | rpl::then(
_listChanges.events() | rpl::map([=] { return total(); })
);
}
int Websites::total() const {
return _list.size();
}
crl::time Websites::lastReceivedTime() {
return _lastReceived;
}
} // namespace Api

View File

@@ -0,0 +1,62 @@
/*
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
*/
#pragma once
#include "mtproto/sender.h"
class ApiWrap;
namespace Main {
class Session;
} // namespace Main
namespace Api {
class Websites final {
public:
explicit Websites(not_null<ApiWrap*> api);
struct Entry {
uint64 hash = 0;
not_null<UserData*> bot;
TimeId activeTime = 0;
QString active, platform, domain, browser, ip, location;
};
using List = std::vector<Entry>;
void reload();
void cancelCurrentRequest();
void requestTerminate(
Fn<void(const MTPBool &result)> &&done,
Fn<void(const MTP::Error &error)> &&fail,
std::optional<uint64> hash = std::nullopt,
UserData *botToBlock = nullptr);
[[nodiscard]] crl::time lastReceivedTime();
[[nodiscard]] List list() const;
[[nodiscard]] rpl::producer<List> listValue() const;
[[nodiscard]] int total() const;
[[nodiscard]] rpl::producer<int> totalValue() const;
private:
not_null<Main::Session*> _session;
MTP::Sender _api;
mtpRequestId _requestId = 0;
List _list;
rpl::event_stream<> _listChanges;
crl::time _lastReceived = 0;
rpl::lifetime _lifetime;
};
} // namespace Api

View File

@@ -0,0 +1,788 @@
/*
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 "api/api_who_reacted.h"
#include "api/api_global_privacy.h"
#include "history/history_item.h"
#include "history/history.h"
#include "data/stickers/data_custom_emoji.h"
#include "data/data_peer.h"
#include "data/data_chat.h"
#include "data/data_channel.h"
#include "data/data_document.h"
#include "data/data_user.h"
#include "data/data_changes.h"
#include "data/data_session.h"
#include "data/data_media_types.h"
#include "data/data_message_reaction_id.h"
#include "data/data_peer_values.h"
#include "lang/lang_keys.h"
#include "main/main_app_config.h"
#include "main/main_session.h"
#include "base/unixtime.h"
#include "base/weak_ptr.h"
#include "ui/controls/who_reacted_context_action.h"
#include "apiwrap.h"
#include "styles/style_chat.h"
#include "styles/style_chat_helpers.h"
namespace Api {
namespace {
constexpr auto kContextReactionsLimit = 50;
using Data::ReactionId;
using WhoReadState = Ui::WhoReadState;
struct Peers {
std::vector<WhoReadPeer> list;
WhoReadState state = WhoReadState::Empty;
friend inline bool operator==(
const Peers &a,
const Peers &b) noexcept = default;
};
struct PeerWithReaction {
WhoReadPeer peerWithDate;
ReactionId reaction;
friend inline bool operator==(
const PeerWithReaction &a,
const PeerWithReaction &b) noexcept = default;
};
struct PeersWithReactions {
std::vector<PeerWithReaction> list;
std::vector<WhoReadPeer> read;
int fullReactionsCount = 0;
WhoReadState state = WhoReadState::Empty;
friend inline bool operator==(
const PeersWithReactions &a,
const PeersWithReactions &b) noexcept = default;
};
struct CachedRead {
CachedRead()
: data(Peers{ .state = WhoReadState::Unknown }) {
}
rpl::variable<Peers> data;
mtpRequestId requestId = 0;
};
struct CachedReacted {
CachedReacted()
: data(PeersWithReactions{ .state = WhoReadState::Unknown }) {
}
rpl::variable<PeersWithReactions> data;
mtpRequestId requestId = 0;
};
struct Context {
base::flat_map<not_null<HistoryItem*>, CachedRead> cachedRead;
base::flat_map<
not_null<HistoryItem*>,
base::flat_map<ReactionId, CachedReacted>> cachedReacted;
base::flat_map<not_null<Main::Session*>, rpl::lifetime> subscriptions;
[[nodiscard]] CachedRead &cacheRead(not_null<HistoryItem*> item) {
const auto i = cachedRead.find(item);
if (i != end(cachedRead)) {
return i->second;
}
return cachedRead.emplace(item, CachedRead()).first->second;
}
[[nodiscard]] CachedReacted &cacheReacted(
not_null<HistoryItem*> item,
const ReactionId &reaction) {
auto &map = cachedReacted[item];
const auto i = map.find(reaction);
if (i != end(map)) {
return i->second;
}
return map.emplace(reaction, CachedReacted()).first->second;
}
};
struct Userpic {
not_null<PeerData*> peer;
TimeId date = 0;
bool dateReacted = false;
QString customEntityData;
mutable Ui::PeerUserpicView view;
mutable InMemoryKey uniqueKey;
};
struct State {
std::vector<Userpic> userpics;
Ui::WhoReadContent current;
base::has_weak_ptr guard;
bool someUserpicsNotLoaded = false;
bool scheduled = false;
};
[[nodiscard]] auto Contexts()
-> base::flat_map<not_null<QWidget*>, std::unique_ptr<Context>> & {
static auto result = base::flat_map<
not_null<QWidget*>,
std::unique_ptr<Context>>();
return result;
}
[[nodiscard]] not_null<Context*> ContextAt(not_null<QWidget*> key) {
auto &contexts = Contexts();
const auto i = contexts.find(key);
if (i != end(contexts)) {
return i->second.get();
}
const auto result = contexts.emplace(
key,
std::make_unique<Context>()).first->second.get();
QObject::connect(key.get(), &QObject::destroyed, [=] {
auto &contexts = Contexts();
const auto i = contexts.find(key);
for (auto &[item, entry] : i->second->cachedRead) {
if (const auto requestId = entry.requestId) {
item->history()->session().api().request(requestId).cancel();
}
}
for (auto &[item, map] : i->second->cachedReacted) {
for (auto &[reaction, entry] : map) {
if (const auto requestId = entry.requestId) {
item->history()->session().api().request(requestId).cancel();
}
}
}
contexts.erase(i);
});
return result;
}
[[nodiscard]] not_null<Context*> PreparedContextAt(
not_null<QWidget*> key,
not_null<Main::Session*> session) {
const auto context = ContextAt(key);
if (context->subscriptions.contains(session)) {
return context;
}
session->changes().messageUpdates(
Data::MessageUpdate::Flag::Destroyed
) | rpl::on_next([=](const Data::MessageUpdate &update) {
const auto i = context->cachedRead.find(update.item);
if (i != end(context->cachedRead)) {
session->api().request(i->second.requestId).cancel();
context->cachedRead.erase(i);
}
const auto j = context->cachedReacted.find(update.item);
if (j != end(context->cachedReacted)) {
for (auto &[reaction, entry] : j->second) {
session->api().request(entry.requestId).cancel();
}
context->cachedReacted.erase(j);
}
}, context->subscriptions[session]);
Data::AmPremiumValue(
session
) | rpl::skip(1) | rpl::filter(
rpl::mappers::_1
) | rpl::on_next([=] {
for (auto &[item, cache] : context->cachedRead) {
if (cache.data.current().state == Ui::WhoReadState::MyHidden) {
cache.data = Peers{ .state = Ui::WhoReadState::Unknown };
}
}
}, context->subscriptions[session]);
session->api().globalPrivacy().hideReadTime(
) | rpl::skip(1) | rpl::filter(
!rpl::mappers::_1
) | rpl::on_next([=] {
for (auto &[item, cache] : context->cachedRead) {
if (cache.data.current().state == Ui::WhoReadState::MyHidden) {
cache.data = Peers{ .state = Ui::WhoReadState::Unknown };
}
}
}, context->subscriptions[session]);
return context;
}
[[nodiscard]] QImage GenerateUserpic(Userpic &userpic, int size) {
size *= style::DevicePixelRatio();
auto result = PeerData::GenerateUserpicImage(
userpic.peer,
userpic.view,
size);
result.setDevicePixelRatio(style::DevicePixelRatio());
return result;
}
[[nodiscard]] Ui::WhoReadType DetectSeenType(not_null<HistoryItem*> item) {
if (const auto media = item->media()) {
if (!media->webpage()) {
if (const auto document = media->document()) {
if (document->isVoiceMessage()) {
return Ui::WhoReadType::Listened;
} else if (document->isVideoMessage()) {
return Ui::WhoReadType::Watched;
}
}
}
}
return Ui::WhoReadType::Seen;
}
[[nodiscard]] rpl::producer<Peers> WhoReadIds(
not_null<HistoryItem*> item,
not_null<QWidget*> context) {
auto weak = base::make_weak(context);
const auto session = &item->history()->session();
return [=](auto consumer) {
if (!weak) {
return rpl::lifetime();
}
const auto context = PreparedContextAt(weak.get(), session);
auto &entry = context->cacheRead(item);
if (entry.requestId) {
} else if (const auto user = item->history()->peer->asUser()) {
entry.requestId = session->api().request(
MTPmessages_GetOutboxReadDate(
user->input(),
MTP_int(item->id)
)
).done([=](const MTPOutboxReadDate &result) {
const auto &data = result.data();
auto &entry = context->cacheRead(item);
entry.requestId = 0;
auto parsed = Peers();
parsed.list.push_back({
.peer = user->id,
.date = data.vdate().v,
});
entry.data = std::move(parsed);
}).fail([=](const MTP::Error &error) {
auto &entry = context->cacheRead(item);
entry.requestId = 0;
if (entry.data.current().state == WhoReadState::Unknown) {
const auto &text = error.type();
entry.data = (text == u"YOUR_PRIVACY_RESTRICTED"_q)
? Peers{ .state = WhoReadState::MyHidden }
: (text == u"USER_PRIVACY_RESTRICTED"_q)
? Peers{ .state = WhoReadState::HisHidden }
: (text == u"MESSAGE_TOO_OLD"_q)
? Peers{ .state = WhoReadState::TooOld }
: Peers{ .state = WhoReadState::Empty };
}
}).send();
} else {
entry.requestId = session->api().request(
MTPmessages_GetMessageReadParticipants(
item->history()->peer->input(),
MTP_int(item->id)
)
).done([=](const MTPVector<MTPReadParticipantDate> &result) {
auto &entry = context->cacheRead(item);
entry.requestId = 0;
auto parsed = Peers();
parsed.list.reserve(result.v.size());
for (const auto &id : result.v) {
parsed.list.push_back({
.peer = UserId(id.data().vuser_id()),
.date = id.data().vdate().v,
});
}
entry.data = std::move(parsed);
}).fail([=] {
auto &entry = context->cacheRead(item);
entry.requestId = 0;
if (entry.data.current().state == WhoReadState::Unknown) {
entry.data = Peers{ .state = WhoReadState::Empty };
}
}).send();
}
return entry.data.value().start_existing(consumer);
};
}
[[nodiscard]] PeersWithReactions WithEmptyReactions(
Peers &&peers) {
auto result = PeersWithReactions{
.list = peers.list | ranges::views::transform([](WhoReadPeer peer) {
return PeerWithReaction{ .peerWithDate = peer };
}) | ranges::to_vector,
.state = peers.state,
};
result.read = std::move(peers.list);
return result;
}
[[nodiscard]] rpl::producer<PeersWithReactions> WhoReactedIds(
not_null<HistoryItem*> item,
const ReactionId &reaction,
not_null<QWidget*> context) {
auto weak = base::make_weak(context);
const auto session = &item->history()->session();
return [=](auto consumer) {
if (!weak) {
return rpl::lifetime();
}
const auto context = PreparedContextAt(weak.get(), session);
auto &entry = context->cacheReacted(item, reaction);
if (!entry.requestId) {
using Flag = MTPmessages_GetMessageReactionsList::Flag;
entry.requestId = session->api().request(
MTPmessages_GetMessageReactionsList(
MTP_flags(reaction.empty()
? Flag(0)
: Flag::f_reaction),
item->history()->peer->input(),
MTP_int(item->id),
ReactionToMTP(reaction),
MTPstring(), // offset
MTP_int(kContextReactionsLimit)
)
).done([=](const MTPmessages_MessageReactionsList &result) {
auto &entry = context->cacheReacted(item, reaction);
entry.requestId = 0;
result.match([&](
const MTPDmessages_messageReactionsList &data) {
session->data().processUsers(data.vusers());
session->data().processChats(data.vchats());
auto parsed = PeersWithReactions{
.fullReactionsCount = data.vcount().v,
};
parsed.list.reserve(data.vreactions().v.size());
for (const auto &vote : data.vreactions().v) {
const auto &data = vote.data();
parsed.list.push_back(PeerWithReaction{
.peerWithDate = {
.peer = peerFromMTP(data.vpeer_id()),
.date = data.vdate().v,
.dateReacted = true,
},
.reaction = Data::ReactionFromMTP(
data.vreaction()),
});
}
entry.data = std::move(parsed);
});
}).fail([=] {
auto &entry = context->cacheReacted(item, reaction);
entry.requestId = 0;
if (entry.data.current().state == WhoReadState::Unknown) {
entry.data = PeersWithReactions{
.state = WhoReadState::Empty,
};
}
}).send();
}
return entry.data.value().start_existing(consumer);
};
}
[[nodiscard]] auto WhoReadOrReactedIds(
not_null<HistoryItem*> item,
not_null<QWidget*> context)
-> rpl::producer<PeersWithReactions> {
return rpl::combine(
WhoReactedIds(item, {}, context),
WhoReadIds(item, context)
) | rpl::map([=](PeersWithReactions &&reacted, Peers &&read) {
if (reacted.state == WhoReadState::Unknown
|| read.state == WhoReadState::Unknown) {
return PeersWithReactions{ .state = WhoReadState::Unknown};
}
auto &list = reacted.list;
for (const auto &peerWithDate : read.list) {
const auto i = ranges::find(
list,
peerWithDate.peer,
[](const PeerWithReaction &p) {
return p.peerWithDate.peer; });
if (i == end(list)) {
list.push_back({ .peerWithDate = peerWithDate });
} else if (!i->peerWithDate.date) {
i->peerWithDate.date = peerWithDate.date;
i->peerWithDate.dateReacted = peerWithDate.dateReacted;
}
}
reacted.read = std::move(read.list);
return std::move(reacted);
});
}
bool UpdateUserpics(
not_null<State*> state,
not_null<HistoryItem*> item,
const std::vector<PeerWithReaction> &ids) {
auto &owner = item->history()->owner();
struct ResolvedPeer {
PeerData *peer = nullptr;
TimeId date = 0;
bool dateReacted = false;
ReactionId reaction;
};
const auto peers = ranges::views::all(
ids
) | ranges::views::transform([&](PeerWithReaction id) {
return ResolvedPeer{
.peer = owner.peerLoaded(id.peerWithDate.peer),
.date = id.peerWithDate.date,
.dateReacted = id.peerWithDate.dateReacted,
.reaction = id.reaction,
};
}) | ranges::views::filter([](ResolvedPeer resolved) {
return resolved.peer != nullptr;
}) | ranges::to_vector;
const auto same = ranges::equal(
state->userpics,
peers,
ranges::equal_to(),
[](const Userpic &u) { return std::pair(u.peer.get(), u.date); },
[](const ResolvedPeer &r) { return std::pair(r.peer, r.date); });
if (same) {
return false;
}
auto &was = state->userpics;
auto now = std::vector<Userpic>();
for (const auto &resolved : peers) {
const auto peer = not_null{ resolved.peer };
const auto &data = ReactionEntityData(resolved.reaction);
const auto i = ranges::find(was, peer, &Userpic::peer);
if (i != end(was) && i->view.cloud) {
i->date = resolved.date;
i->dateReacted = resolved.dateReacted;
now.push_back(std::move(*i));
now.back().customEntityData = data;
continue;
}
now.push_back(Userpic{
.peer = peer,
.date = resolved.date,
.dateReacted = resolved.dateReacted,
.customEntityData = data,
});
auto &userpic = now.back();
userpic.uniqueKey = peer->userpicUniqueKey(userpic.view);
peer->loadUserpic();
}
was = std::move(now);
return true;
}
void RegenerateUserpics(not_null<State*> state, int small, int large) {
Expects(state->userpics.size() == state->current.participants.size());
state->someUserpicsNotLoaded = false;
const auto count = int(state->userpics.size());
for (auto i = 0; i != count; ++i) {
auto &userpic = state->userpics[i];
auto &participant = state->current.participants[i];
const auto peer = userpic.peer;
const auto key = peer->userpicUniqueKey(userpic.view);
if (peer->hasUserpic() && peer->useEmptyUserpic(userpic.view)) {
state->someUserpicsNotLoaded = true;
}
if (userpic.uniqueKey == key) {
continue;
}
participant.userpicKey = userpic.uniqueKey = key;
participant.userpicLarge = GenerateUserpic(userpic, large);
if (i < Ui::WhoReadParticipant::kMaxSmallUserpics) {
participant.userpicSmall = GenerateUserpic(userpic, small);
}
}
}
void RegenerateParticipants(not_null<State*> state, int small, int large) {
const auto currentDate = QDateTime::currentDateTime();
auto old = base::take(state->current.participants);
auto &now = state->current.participants;
now.reserve(state->userpics.size());
for (auto &userpic : state->userpics) {
const auto peer = userpic.peer;
const auto date = userpic.date;
const auto id = peer->id.value;
const auto was = ranges::find(old, id, &Ui::WhoReadParticipant::id);
if (was != end(old)) {
was->name = peer->name();
was->date = FormatReadDate(date, currentDate);
was->dateReacted = userpic.dateReacted;
now.push_back(std::move(*was));
continue;
}
now.push_back({
.name = peer->name(),
.date = FormatReadDate(date, currentDate),
.dateReacted = userpic.dateReacted,
.customEntityData = userpic.customEntityData,
.userpicLarge = GenerateUserpic(userpic, large),
.userpicKey = userpic.uniqueKey,
.id = id,
});
if (now.size() <= Ui::WhoReadParticipant::kMaxSmallUserpics) {
now.back().userpicSmall = GenerateUserpic(userpic, small);
}
}
RegenerateUserpics(state, small, large);
}
rpl::producer<Ui::WhoReadContent> WhoReacted(
not_null<HistoryItem*> item,
const ReactionId &reaction,
not_null<QWidget*> context,
const style::WhoRead &st,
std::shared_ptr<WhoReadList> whoReadIds) {
const auto small = st.userpics.size;
const auto large = st.photoSize;
return [=](auto consumer) {
auto lifetime = rpl::lifetime();
const auto resolveWhoRead = reaction.empty()
&& WhoReadExists(item);
const auto state = lifetime.make_state<State>();
const auto pushNext = [=] {
consumer.put_next_copy(state->current);
};
const auto resolveWhoReacted = !reaction.empty()
|| item->canViewReactions();
auto idsWithReactions = (resolveWhoRead && resolveWhoReacted)
? WhoReadOrReactedIds(item, context)
: resolveWhoRead
? (WhoReadIds(item, context) | rpl::map(WithEmptyReactions))
: WhoReactedIds(item, reaction, context);
state->current.type = resolveWhoRead
? DetectSeenType(item)
: Ui::WhoReadType::Reacted;
if (resolveWhoReacted) {
const auto &list = item->reactions();
state->current.fullReactionsCount = [&] {
if (reaction.empty()) {
return ranges::accumulate(
list,
0,
ranges::plus{},
&Data::MessageReaction::count);
}
const auto i = ranges::find(
list,
reaction,
&Data::MessageReaction::id);
return (i != end(list)) ? i->count : 0;
}();
state->current.singleCustomEntityData = ReactionEntityData(
!reaction.empty()
? reaction
: (list.size() == 1)
? list.front().id
: ReactionId());
}
std::move(
idsWithReactions
) | rpl::on_next([=](PeersWithReactions &&peers) {
if (peers.state == WhoReadState::Unknown) {
state->userpics.clear();
consumer.put_next(Ui::WhoReadContent{
.type = state->current.type,
.fullReactionsCount = state->current.fullReactionsCount,
.fullReadCount = state->current.fullReadCount,
.state = WhoReadState::Unknown,
});
return;
}
state->current.state = peers.state;
state->current.fullReadCount = int(peers.read.size());
state->current.fullReactionsCount = peers.fullReactionsCount;
if (whoReadIds) {
const auto reacted = peers.list.size() - ranges::count(
peers.list,
ReactionId(),
&PeerWithReaction::reaction);
whoReadIds->list = (peers.read.size() > reacted)
? std::move(peers.read)
: std::vector<WhoReadPeer>();
}
if (UpdateUserpics(state, item, peers.list)) {
RegenerateParticipants(state, small, large);
pushNext();
} else if (peers.list.empty()) {
pushNext();
}
}, lifetime);
item->history()->session().downloaderTaskFinished(
) | rpl::filter([=] {
return state->someUserpicsNotLoaded && !state->scheduled;
}) | rpl::on_next([=] {
for (const auto &userpic : state->userpics) {
if (userpic.peer->userpicUniqueKey(userpic.view)
!= userpic.uniqueKey) {
state->scheduled = true;
crl::on_main(&state->guard, [=] {
state->scheduled = false;
RegenerateUserpics(state, small, large);
pushNext();
});
return;
}
}
}, lifetime);
return lifetime;
};
}
} // namespace
QString FormatReadDate(TimeId date, const QDateTime &now) {
if (!date) {
return {};
}
const auto parsed = base::unixtime::parse(date);
const auto readDate = parsed.date();
const auto nowDate = now.date();
if (readDate == nowDate) {
return tr::lng_mediaview_today(
tr::now,
lt_time,
QLocale().toString(parsed.time(), QLocale::ShortFormat));
} else if (readDate.addDays(1) == nowDate) {
return tr::lng_mediaview_yesterday(
tr::now,
lt_time,
QLocale().toString(parsed.time(), QLocale::ShortFormat));
}
return tr::lng_mediaview_date_time(
tr::now,
lt_date,
tr::lng_month_day(
tr::now,
lt_month,
Lang::MonthDay(readDate.month())(tr::now),
lt_day,
QString::number(readDate.day())),
lt_time,
QLocale().toString(parsed.time(), QLocale::ShortFormat));
}
bool WhoReadExists(not_null<HistoryItem*> item) {
if (!item->out()) {
return false;
}
const auto type = DetectSeenType(item);
const auto thread = item->topic()
? (Data::Thread*)item->topic()
: item->history();
const auto unseen = (type == Ui::WhoReadType::Seen)
? item->unread(thread)
: item->isUnreadMedia();
if (unseen) {
return false;
}
const auto history = item->history();
const auto peer = history->peer;
if (const auto user = peer->asUser()) {
if (user->isSelf()
|| user->isBot()
|| user->isServiceUser()
|| user->readDatesPrivate()) {
return false;
}
const auto &appConfig = peer->session().appConfig();
const auto expirePeriod = appConfig.get<int>(
"pm_read_date_expire_period",
7 * 86400);
if (item->date() + int64(expirePeriod) <= int64(base::unixtime::now())) {
return false;
}
return true;
}
const auto chat = peer->asChat();
const auto megagroup = peer->asMegagroup();
if ((!chat && !megagroup)
|| (megagroup
&& (megagroup->flags() & ChannelDataFlag::ParticipantsHidden))
|| (megagroup && megagroup->isMonoforum())) {
return false;
}
const auto &appConfig = peer->session().appConfig();
const auto expirePeriod = appConfig.get<int>(
"chat_read_mark_expire_period",
7 * 86400);
if (item->date() + int64(expirePeriod) <= int64(base::unixtime::now())) {
return false;
}
const auto maxCount = appConfig.get<int>(
"chat_read_mark_size_threshold",
50);
const auto count = megagroup ? megagroup->membersCount() : chat->count;
if (count <= 0 || count > maxCount) {
return false;
}
return true;
}
bool WhoReactedExists(
not_null<HistoryItem*> item,
WhoReactedList list) {
if (item->canViewReactions() || WhoReadExists(item)) {
return true;
}
return (list == WhoReactedList::One) && item->history()->peer->isUser();
}
rpl::producer<Ui::WhoReadContent> WhoReacted(
not_null<HistoryItem*> item,
not_null<QWidget*> context,
const style::WhoRead &st,
std::shared_ptr<WhoReadList> whoReadIds) {
return WhoReacted(item, {}, context, st, std::move(whoReadIds));
}
rpl::producer<Ui::WhoReadContent> WhoReacted(
not_null<HistoryItem*> item,
const Data::ReactionId &reaction,
not_null<QWidget*> context,
const style::WhoRead &st) {
return WhoReacted(item, reaction, context, st, nullptr);
}
[[nodiscard]] rpl::producer<Ui::WhoReadContent> WhenDate(
not_null<PeerData*> author,
TimeId date,
Ui::WhoReadType type) {
return rpl::single(Ui::WhoReadContent{
.participants = { Ui::WhoReadParticipant{
.name = author->name(),
.date = FormatReadDate(date, QDateTime::currentDateTime()),
.id = author->id.value,
} },
.type = type,
.fullReadCount = 1,
});
}
rpl::producer<Ui::WhoReadContent> WhenEdited(
not_null<PeerData*> author,
TimeId date) {
return WhenDate(author, date, Ui::WhoReadType::Edited);
}
rpl::producer<Ui::WhoReadContent> WhenOriginal(
not_null<PeerData*> author,
TimeId date) {
return WhenDate(author, date, Ui::WhoReadType::Original);
}
} // namespace Api

Some files were not shown because too many files have changed in this diff Show More