/* This file is part of Telegram Desktop, the official desktop application for the Telegram messaging service. For license and copyright information please follow this link: https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "data/components/gift_auctions.h" #include "api/api_hash.h" #include "api/api_premium.h" #include "api/api_text_entities.h" #include "apiwrap.h" #include "data/data_session.h" #include "main/main_session.h" namespace Data { GiftAuctions::GiftAuctions(not_null session) : _session(session) , _timer([=] { checkSubscriptions(); }) { crl::on_main(_session, [=] { rpl::merge( _session->data().chatsListChanges(), _session->data().chatsListLoadedEvents() ) | rpl::filter( !rpl::mappers::_1 ) | rpl::take(1) | rpl::on_next([=] { requestActive(); }, _lifetime); }); } GiftAuctions::~GiftAuctions() = default; rpl::producer GiftAuctions::state(const QString &slug) { return [=](auto consumer) { auto lifetime = rpl::lifetime(); auto &entry = _map[slug]; if (!entry) { entry = std::make_unique(); } const auto raw = entry.get(); raw->changes.events() | rpl::on_next([=] { consumer.put_next_copy(raw->state); }, lifetime); const auto now = crl::now(); if (raw->state.subscribedTill < 0 || raw->state.subscribedTill >= now) { consumer.put_next_copy(raw->state); } else if (raw->state.subscribedTill >= 0) { request(slug); } return lifetime; }; } void GiftAuctions::apply(const MTPDupdateStarGiftAuctionState &data) { if (const auto entry = find(data.vgift_id().v)) { const auto was = myStateKey(entry->state); apply(entry, data.vstate()); entry->changes.fire({}); if (was != myStateKey(entry->state)) { _activeChanged.fire({}); } } else { requestActive(); } } void GiftAuctions::apply(const MTPDupdateStarGiftAuctionUserState &data) { if (const auto entry = find(data.vgift_id().v)) { const auto was = myStateKey(entry->state); apply(entry, data.vuser_state()); entry->changes.fire({}); if (was != myStateKey(entry->state)) { _activeChanged.fire({}); } } else { requestActive(); } } void GiftAuctions::requestAcquired( uint64 giftId, Fn)> done) { Expects(done != nullptr); _session->api().request(MTPpayments_GetStarGiftAuctionAcquiredGifts( MTP_long(giftId) )).done([=](const MTPpayments_StarGiftAuctionAcquiredGifts &result) { const auto &data = result.data(); const auto owner = &_session->data(); owner->processUsers(data.vusers()); owner->processChats(data.vchats()); const auto &list = data.vgifts().v; auto gifts = std::vector(); gifts.reserve(list.size()); for (const auto &gift : list) { const auto &data = gift.data(); gifts.push_back({ .to = owner->peer(peerFromMTP(data.vpeer())), .message = (data.vmessage() ? Api::ParseTextWithEntities(_session, *data.vmessage()) : TextWithEntities()), .date = data.vdate().v, .bidAmount = int64(data.vbid_amount().v), .round = data.vround().v, .number = data.vgift_num().value_or_empty(), .position = data.vpos().v, .nameHidden = data.is_name_hidden(), }); } if (const auto entry = find(giftId)) { const auto count = int(gifts.size()); if (entry->state.my.gotCount != count) { entry->state.my.gotCount = count; entry->changes.fire({}); } } done(std::move(gifts)); }).fail([=] { done({}); }).send(); } std::optional GiftAuctions::attributes( uint64 giftId) const { const auto i = _attributes.find(giftId); return (i != end(_attributes) && i->second.waiters.empty()) ? i->second.lists : std::optional(); } void GiftAuctions::requestAttributes(uint64 giftId, Fn ready) { auto &entry = _attributes[giftId]; entry.waiters.push_back(std::move(ready)); if (entry.waiters.size() > 1) { return; } _session->api().request(MTPpayments_GetStarGiftUpgradeAttributes( MTP_long(giftId) )).done([=](const MTPpayments_StarGiftUpgradeAttributes &result) { const auto &attributes = result.data().vattributes().v; auto &entry = _attributes[giftId]; auto &info = entry.lists; info.models.reserve(attributes.size()); info.patterns.reserve(attributes.size()); info.backdrops.reserve(attributes.size()); for (const auto &attribute : attributes) { attribute.match([&](const MTPDstarGiftAttributeModel &data) { info.models.push_back(Api::FromTL(_session, data)); }, [&](const MTPDstarGiftAttributePattern &data) { info.patterns.push_back(Api::FromTL(_session, data)); }, [&](const MTPDstarGiftAttributeBackdrop &data) { info.backdrops.push_back(Api::FromTL(data)); }, [](const MTPDstarGiftAttributeOriginalDetails &data) { }); } for (const auto &ready : base::take(entry.waiters)) { ready(); } }).fail([=] { for (const auto &ready : base::take(_attributes[giftId].waiters)) { ready(); } }).send(); } rpl::producer GiftAuctions::active() const { return _activeChanged.events_starting_with_copy( rpl::empty ) | rpl::map([=] { return collectActive(); }); } rpl::producer GiftAuctions::hasActiveChanges() const { const auto has = hasActive(); return _activeChanged.events( ) | rpl::map([=] { return hasActive(); }) | rpl::combine_previous( has ) | rpl::filter([=](bool previous, bool current) { return previous != current; }) | rpl::map([=](bool previous, bool current) { return current; }); } bool GiftAuctions::hasActive() const { for (const auto &[slug, entry] : _map) { if (myStateKey(entry->state)) { return true; } } return false; } void GiftAuctions::checkSubscriptions() { const auto now = crl::now(); auto next = crl::time(); for (const auto &[slug, entry] : _map) { const auto raw = entry.get(); const auto till = raw->state.subscribedTill; if (till <= 0 || !raw->changes.has_consumers()) { continue; } else if (till <= now) { request(slug); } else { const auto timeout = till - now; if (!next || timeout < next) { next = timeout; } } } if (next) { _timer.callOnce(next); } } auto GiftAuctions::myStateKey(const GiftAuctionState &state) const -> MyStateKey { if (!state.my.bid) { return {}; } auto min = 0; for (const auto &level : state.bidLevels) { if (level.position > state.gift->auctionGiftsPerRound) { break; } else if (!min || min > level.amount) { min = level.amount; } } return { .bid = int(state.my.bid), .position = MyAuctionPosition(state), .version = state.version, }; } ActiveAuctions GiftAuctions::collectActive() const { auto result = ActiveAuctions(); result.list.reserve(_map.size()); for (const auto &[slug, entry] : _map) { const auto raw = &entry->state; if (raw->gift && raw->my.date) { result.list.push_back(raw); } } return result; } uint64 GiftAuctions::countActiveHash() const { auto result = Api::HashInit(); for (const auto &active : collectActive().list) { Api::HashUpdate(result, active->version); Api::HashUpdate(result, active->my.date); } return Api::HashFinalize(result); } void GiftAuctions::requestActive() { if (_activeRequestId) { return; } _activeRequestId = _session->api().request( MTPpayments_GetStarGiftActiveAuctions(MTP_long(countActiveHash())) ).done([=](const MTPpayments_StarGiftActiveAuctions &result) { result.match([=](const MTPDpayments_starGiftActiveAuctions &data) { const auto owner = &_session->data(); owner->processUsers(data.vusers()); owner->processChats(data.vchats()); auto giftsFound = base::flat_set(); const auto &list = data.vauctions().v; giftsFound.reserve(list.size()); for (const auto &auction : list) { const auto &data = auction.data(); auto gift = Api::FromTL(_session, data.vgift()); const auto slug = gift ? gift->auctionSlug : QString(); if (slug.isEmpty()) { LOG(("Api Error: Bad auction gift.")); continue; } auto &entry = _map[slug]; if (!entry) { entry = std::make_unique(); } const auto raw = entry.get(); if (!raw->state.gift) { raw->state.gift = std::move(gift); } apply(raw, data.vstate()); apply(raw, data.vuser_state()); giftsFound.emplace(slug); } for (const auto &[slug, entry] : _map) { const auto my = &entry->state.my; if (my->date && !giftsFound.contains(slug)) { my->to = nullptr; my->minBidAmount = 0; my->bid = 0; my->date = 0; my->returned = false; giftsFound.emplace(slug); } } for (const auto &slug : giftsFound) { _map[slug]->changes.fire({}); } _activeChanged.fire({}); }, [](const MTPDpayments_starGiftActiveAuctionsNotModified &) { }); }).send(); } void GiftAuctions::request(const QString &slug) { auto &entry = _map[slug]; Assert(entry != nullptr); const auto raw = entry.get(); if (raw->requested) { return; } raw->requested = true; _session->api().request(MTPpayments_GetStarGiftAuctionState( MTP_inputStarGiftAuctionSlug(MTP_string(slug)), MTP_int(raw->state.version) )).done([=](const MTPpayments_StarGiftAuctionState &result) { raw->requested = false; const auto &data = result.data(); _session->data().processUsers(data.vusers()); _session->data().processChats(data.vchats()); raw->state.gift = Api::FromTL(_session, data.vgift()); if (!raw->state.gift) { return; } const auto timeout = data.vtimeout().v; const auto ms = timeout * crl::time(1000); raw->state.subscribedTill = ms ? (crl::now() + ms) : -1; const auto was = myStateKey(raw->state); apply(raw, data.vstate()); apply(raw, data.vuser_state()); if (raw->changes.has_consumers()) { raw->changes.fire({}); if (ms && (!_timer.isActive() || _timer.remainingTime() > ms)) { _timer.callOnce(ms); } } if (was != myStateKey(raw->state)) { _activeChanged.fire({}); } }).send(); } GiftAuctions::Entry *GiftAuctions::find(uint64 giftId) const { for (const auto &[slug, entry] : _map) { if (entry->state.gift && entry->state.gift->id == giftId) { return entry.get(); } } return nullptr; } void GiftAuctions::apply( not_null entry, const MTPStarGiftAuctionState &state) { apply(&entry->state, state); } void GiftAuctions::apply( not_null entry, const MTPStarGiftAuctionState &state) { Expects(entry->gift.has_value()); state.match([&](const MTPDstarGiftAuctionState &data) { const auto version = data.vversion().v; if (entry->version >= version) { return; } const auto owner = &_session->data(); entry->startDate = data.vstart_date().v; entry->endDate = data.vend_date().v; entry->minBidAmount = data.vmin_bid_amount().v; const auto &levels = data.vbid_levels().v; entry->bidLevels.clear(); entry->bidLevels.reserve(levels.size()); for (const auto &level : levels) { auto &bid = entry->bidLevels.emplace_back(); const auto &data = level.data(); bid.amount = data.vamount().v; bid.position = data.vpos().v; bid.date = data.vdate().v; } const auto &top = data.vtop_bidders().v; entry->topBidders.clear(); entry->topBidders.reserve(top.size()); for (const auto &user : top) { entry->topBidders.push_back(owner->user(UserId(user.v))); } entry->nextRoundAt = data.vnext_round_at().v; entry->giftsLeft = data.vgifts_left().v; entry->currentRound = data.vcurrent_round().v; entry->totalRounds = data.vtotal_rounds().v; const auto &rounds = data.vrounds().v; entry->roundParameters.clear(); entry->roundParameters.reserve(rounds.size()); for (const auto &round : rounds) { round.match([&](const MTPDstarGiftAuctionRound &data) { entry->roundParameters.push_back({ .number = data.vnum().v, .duration = data.vduration().v, }); }, [&](const MTPDstarGiftAuctionRoundExtendable &data) { entry->roundParameters.push_back({ .number = data.vnum().v, .duration = data.vduration().v, .extendTop = data.vextend_top().v, .extendDuration = data.vextend_window().v, }); }); } entry->averagePrice = 0; }, [&](const MTPDstarGiftAuctionStateFinished &data) { entry->averagePrice = data.vaverage_price().v; entry->startDate = data.vstart_date().v; entry->endDate = data.vend_date().v; entry->minBidAmount = 0; entry->nextRoundAt = entry->currentRound = entry->totalRounds = entry->giftsLeft = entry->version = 0; }, [&](const MTPDstarGiftAuctionStateNotModified &data) { }); } void GiftAuctions::apply( not_null entry, const MTPStarGiftAuctionUserState &state) { apply(&entry->state.my, state); } void GiftAuctions::apply( not_null entry, const MTPStarGiftAuctionUserState &state) { const auto &data = state.data(); entry->to = data.vbid_peer() ? _session->data().peer(peerFromMTP(*data.vbid_peer())).get() : nullptr; entry->minBidAmount = data.vmin_bid_amount().value_or(0); entry->bid = data.vbid_amount().value_or(0); entry->date = data.vbid_date().value_or(0); entry->gotCount = data.vacquired_count().v; entry->returned = data.is_returned(); } int MyAuctionPosition(const GiftAuctionState &state) { const auto &levels = state.bidLevels; for (auto i = begin(levels), e = end(levels); i != e; ++i) { if (i->amount < state.my.bid || (i->amount == state.my.bid && i->date >= state.my.date)) { return i->position; } } return (levels.empty() ? 0 : levels.back().position) + 1; } } // namespace Data