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,217 @@
/*
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/business/data_business_chatbots.h"
#include "apiwrap.h"
#include "boxes/peers/edit_peer_permissions_box.h"
#include "data/business/data_business_common.h"
#include "data/business/data_business_info.h"
#include "data/data_session.h"
#include "data/data_user.h"
#include "lang/lang_keys.h"
#include "main/main_session.h"
namespace Data {
Chatbots::Chatbots(not_null<Session*> owner)
: _owner(owner) {
}
Chatbots::~Chatbots() = default;
void Chatbots::preload() {
if (_loaded || _requestId) {
return;
}
_requestId = _owner->session().api().request(
MTPaccount_GetConnectedBots()
).done([=](const MTPaccount_ConnectedBots &result) {
_requestId = 0;
_loaded = true;
const auto &data = result.data();
_owner->processUsers(data.vusers());
const auto &list = data.vconnected_bots().v;
if (!list.isEmpty()) {
const auto &bot = list.front().data();
const auto botId = bot.vbot_id().v;
_settings = ChatbotsSettings{
.bot = _owner->session().data().user(botId),
.recipients = FromMTP(_owner, bot.vrecipients()),
.permissions = FromMTP(bot.vrights()),
};
} else {
_settings.force_assign(ChatbotsSettings());
}
}).fail([=](const MTP::Error &error) {
_requestId = 0;
LOG(("API Error: Could not get connected bots %1 (%2)"
).arg(error.code()
).arg(error.type()));
}).send();
}
bool Chatbots::loaded() const {
return _loaded;
}
const ChatbotsSettings &Chatbots::current() const {
return _settings.current();
}
rpl::producer<ChatbotsSettings> Chatbots::changes() const {
return _settings.changes();
}
rpl::producer<ChatbotsSettings> Chatbots::value() const {
return _settings.value();
}
void Chatbots::save(
ChatbotsSettings settings,
Fn<void()> done,
Fn<void(QString)> fail) {
const auto was = _settings.current();
if (was == settings) {
return;
} else if (was.bot || settings.bot) {
using Flag = MTPaccount_UpdateConnectedBot::Flag;
const auto api = &_owner->session().api();
api->request(MTPaccount_UpdateConnectedBot(
MTP_flags(!settings.bot ? Flag::f_deleted : Flag::f_rights),
ToMTP(settings.permissions),
(settings.bot ? settings.bot : was.bot)->inputUser(),
ForBotsToMTP(settings.recipients)
)).done([=](const MTPUpdates &result) {
api->applyUpdates(result);
if (done) {
done();
}
}).fail([=](const MTP::Error &error) {
_settings = was;
if (fail) {
fail(error.type());
}
}).send();
}
_settings = settings;
}
void Chatbots::togglePaused(not_null<PeerData*> peer, bool paused) {
const auto type = paused
? SentRequestType::Pause
: SentRequestType::Unpause;
const auto api = &_owner->session().api();
const auto i = _sentRequests.find(peer);
if (i != end(_sentRequests)) {
const auto already = i->second.type;
if (already == SentRequestType::Remove || already == type) {
return;
}
api->request(i->second.requestId).cancel();
_sentRequests.erase(i);
}
const auto id = api->request(MTPaccount_ToggleConnectedBotPaused(
peer->input(),
MTP_bool(paused)
)).done([=] {
if (_sentRequests[peer].type != type) {
return;
} else if (const auto settings = peer->barSettings()) {
peer->setBarSettings(paused
? ((*settings | PeerBarSetting::BusinessBotPaused)
& ~PeerBarSetting::BusinessBotCanReply)
: ((*settings & ~PeerBarSetting::BusinessBotPaused)
| PeerBarSetting::BusinessBotCanReply));
} else {
api->requestPeerSettings(peer);
}
_sentRequests.remove(peer);
}).fail([=] {
if (_sentRequests[peer].type != type) {
return;
}
api->requestPeerSettings(peer);
_sentRequests.remove(peer);
}).send();
_sentRequests[peer] = SentRequest{ type, id };
}
void Chatbots::removeFrom(not_null<PeerData*> peer) {
const auto type = SentRequestType::Remove;
const auto api = &_owner->session().api();
const auto i = _sentRequests.find(peer);
if (i != end(_sentRequests)) {
const auto already = i->second.type;
if (already == type) {
return;
}
api->request(i->second.requestId).cancel();
_sentRequests.erase(i);
}
const auto id = api->request(MTPaccount_DisablePeerConnectedBot(
peer->input()
)).done([=] {
if (_sentRequests[peer].type != type) {
return;
} else if (const auto settings = peer->barSettings()) {
peer->clearBusinessBot();
} else {
api->requestPeerSettings(peer);
}
_sentRequests.remove(peer);
reload();
}).fail([=] {
api->requestPeerSettings(peer);
_sentRequests.remove(peer);
}).send();
_sentRequests[peer] = SentRequest{ type, id };
}
void Chatbots::reload() {
_loaded = false;
_owner->session().api().request(base::take(_requestId)).cancel();
preload();
}
EditFlagsDescriptor<ChatbotsPermissions> ChatbotsPermissionsLabels() {
using Flag = ChatbotsPermission;
using PermissionLabel = EditFlagsLabel<ChatbotsPermissions>;
auto messages = std::vector<PermissionLabel>{
{ Flag::ViewMessages, tr::lng_chatbots_read(tr::now) },
{ Flag::ReplyToMessages, tr::lng_chatbots_reply(tr::now) },
{ Flag::MarkAsRead, tr::lng_chatbots_mark_as_read(tr::now) },
{ Flag::DeleteSent, tr::lng_chatbots_delete_sent(tr::now) },
{ Flag::DeleteReceived, tr::lng_chatbots_delete_received(tr::now) },
};
auto manage = std::vector<PermissionLabel>{
{ Flag::EditName, tr::lng_chatbots_edit_name(tr::now) },
{ Flag::EditBio, tr::lng_chatbots_edit_bio(tr::now) },
{ Flag::EditUserpic, tr::lng_chatbots_edit_userpic(tr::now) },
{ Flag::EditUsername, tr::lng_chatbots_edit_username(tr::now) },
};
auto gifts = std::vector<PermissionLabel>{
{ Flag::ViewGifts, tr::lng_chatbots_view_gifts(tr::now) },
{ Flag::SellGifts, tr::lng_chatbots_sell_gifts(tr::now) },
{ Flag::GiftSettings, tr::lng_chatbots_gift_settings(tr::now) },
{ Flag::TransferGifts, tr::lng_chatbots_transfer_gifts(tr::now) },
{ Flag::TransferStars, tr::lng_chatbots_transfer_stars(tr::now) },
};
auto stories = std::vector<PermissionLabel>{
{ Flag::ManageStories, tr::lng_chatbots_manage_stories(tr::now) },
};
return { .labels = {
{ tr::lng_chatbots_manage_messages(), std::move(messages) },
{ tr::lng_chatbots_manage_profile(), std::move(manage) },
{ tr::lng_chatbots_manage_gifts(), std::move(gifts) },
{ std::nullopt, std::move(stories) },
}, .st = nullptr };
}
} // namespace Data

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
#include "data/business/data_business_common.h"
class UserData;
template <typename Flags>
struct EditFlagsDescriptor;
namespace Data {
class Session;
struct ChatbotsSettings {
UserData *bot = nullptr;
BusinessRecipients recipients;
ChatbotsPermissions permissions;
friend inline bool operator==(
const ChatbotsSettings &,
const ChatbotsSettings &) = default;
};
class Chatbots final {
public:
explicit Chatbots(not_null<Session*> owner);
~Chatbots();
void preload();
[[nodiscard]] bool loaded() const;
[[nodiscard]] const ChatbotsSettings &current() const;
[[nodiscard]] rpl::producer<ChatbotsSettings> changes() const;
[[nodiscard]] rpl::producer<ChatbotsSettings> value() const;
void save(
ChatbotsSettings settings,
Fn<void()> done,
Fn<void(QString)> fail);
void togglePaused(not_null<PeerData*> peer, bool paused);
void removeFrom(not_null<PeerData*> peer);
private:
enum class SentRequestType {
Pause,
Unpause,
Remove,
};
struct SentRequest {
SentRequestType type = SentRequestType::Pause;
mtpRequestId requestId = 0;
};
void reload();
const not_null<Session*> _owner;
rpl::variable<ChatbotsSettings> _settings;
mtpRequestId _requestId = 0;
bool _loaded = false;
base::flat_map<not_null<PeerData*>, SentRequest> _sentRequests;
};
[[nodiscard]] auto ChatbotsPermissionsLabels()
-> EditFlagsDescriptor<ChatbotsPermissions>;
} // namespace Data

View File

@@ -0,0 +1,398 @@
/*
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/business/data_business_common.h"
#include "data/data_document.h"
#include "data/data_session.h"
#include "data/data_user.h"
namespace Data {
namespace {
constexpr auto kDay = WorkingInterval::kDay;
constexpr auto kWeek = WorkingInterval::kWeek;
constexpr auto kInNextDayMax = WorkingInterval::kInNextDayMax;
[[nodiscard]] WorkingIntervals SortAndMerge(WorkingIntervals intervals) {
auto &list = intervals.list;
ranges::sort(list, ranges::less(), &WorkingInterval::start);
for (auto i = 0, count = int(list.size()); i != count; ++i) {
if (i && list[i] && list[i -1] && list[i].start <= list[i - 1].end) {
list[i - 1] = list[i - 1].united(list[i]);
list[i] = {};
}
if (!list[i]) {
list.erase(list.begin() + i);
--i;
--count;
}
}
return intervals;
}
[[nodiscard]] WorkingIntervals MoveTailToFront(WorkingIntervals intervals) {
auto &list = intervals.list;
auto after = WorkingInterval{ kWeek, kWeek + kDay };
while (!list.empty()) {
if (const auto tail = list.back().intersected(after)) {
list.back().end = tail.start;
if (!list.back()) {
list.pop_back();
}
list.insert(begin(list), tail.shifted(-kWeek));
} else {
break;
}
}
return intervals;
}
template <typename Flag>
auto RecipientsFlags(const BusinessRecipients &data) {
const auto &chats = data.allButExcluded
? data.excluded
: data.included;
using Type = BusinessChatType;
return Flag()
| ((chats.types & Type::NewChats) ? Flag::f_new_chats : Flag())
| ((chats.types & Type::ExistingChats)
? Flag::f_existing_chats
: Flag())
| ((chats.types & Type::Contacts) ? Flag::f_contacts : Flag())
| ((chats.types & Type::NonContacts) ? Flag::f_non_contacts : Flag())
| (chats.list.empty() ? Flag() : Flag::f_users)
| (data.allButExcluded ? Flag::f_exclude_selected : Flag());
}
} // namespace
BusinessRecipients BusinessRecipients::MakeValid(BusinessRecipients value) {
if (value.included.empty()) {
value.allButExcluded = true;
}
return value;
}
MTPInputBusinessRecipients ForMessagesToMTP(const BusinessRecipients &data) {
using Flag = MTPDinputBusinessRecipients::Flag;
const auto &chats = data.allButExcluded ? data.excluded : data.included;
return MTP_inputBusinessRecipients(
MTP_flags(RecipientsFlags<Flag>(data)),
MTP_vector_from_range(chats.list
| ranges::views::transform(&UserData::inputUser)));
}
MTPInputBusinessBotRecipients ForBotsToMTP(const BusinessRecipients &data) {
using Flag = MTPDinputBusinessBotRecipients::Flag;
const auto &chats = data.allButExcluded ? data.excluded : data.included;
return MTP_inputBusinessBotRecipients(
MTP_flags(RecipientsFlags<Flag>(data)
| ((data.allButExcluded || data.excluded.empty())
? Flag()
: Flag::f_exclude_users)),
MTP_vector_from_range(chats.list
| ranges::views::transform(&UserData::inputUser)),
MTP_vector_from_range(data.excluded.list
| ranges::views::transform(&UserData::inputUser)));
}
BusinessRecipients FromMTP(
not_null<Session*> owner,
const MTPBusinessRecipients &recipients) {
using Type = BusinessChatType;
const auto &data = recipients.data();
auto result = BusinessRecipients{
.allButExcluded = data.is_exclude_selected(),
};
auto &chats = result.allButExcluded
? result.excluded
: result.included;
chats.types = Type()
| (data.is_new_chats() ? Type::NewChats : Type())
| (data.is_existing_chats() ? Type::ExistingChats : Type())
| (data.is_contacts() ? Type::Contacts : Type())
| (data.is_non_contacts() ? Type::NonContacts : Type());
if (const auto users = data.vusers()) {
for (const auto &userId : users->v) {
chats.list.push_back(owner->user(UserId(userId.v)));
}
}
return result;
}
BusinessRecipients FromMTP(
not_null<Session*> owner,
const MTPBusinessBotRecipients &recipients) {
using Type = BusinessChatType;
const auto &data = recipients.data();
auto result = BusinessRecipients{
.allButExcluded = data.is_exclude_selected(),
};
auto &chats = result.allButExcluded
? result.excluded
: result.included;
chats.types = Type()
| (data.is_new_chats() ? Type::NewChats : Type())
| (data.is_existing_chats() ? Type::ExistingChats : Type())
| (data.is_contacts() ? Type::Contacts : Type())
| (data.is_non_contacts() ? Type::NonContacts : Type());
if (const auto users = data.vusers()) {
for (const auto &userId : users->v) {
chats.list.push_back(owner->user(UserId(userId.v)));
}
}
if (!result.allButExcluded) {
if (const auto excluded = data.vexclude_users()) {
for (const auto &userId : excluded->v) {
result.excluded.list.push_back(
owner->user(UserId(userId.v)));
}
}
}
return result;
}
ChatbotsPermissions FromMTP(const MTPBusinessBotRights &rights) {
using Flag = ChatbotsPermission;
const auto &data = rights.data();
return Flag::ViewMessages
| (data.is_reply() ? Flag::ReplyToMessages : Flag())
| (data.is_read_messages() ? Flag::MarkAsRead : Flag())
| (data.is_delete_sent_messages() ? Flag::DeleteSent : Flag())
| (data.is_delete_received_messages() ? Flag::DeleteReceived : Flag())
| (data.is_edit_name() ? Flag::EditName : Flag())
| (data.is_edit_bio() ? Flag::EditBio : Flag())
| (data.is_edit_profile_photo() ? Flag::EditUserpic : Flag())
| (data.is_edit_username() ? Flag::EditUsername : Flag())
| (data.is_view_gifts() ? Flag::ViewGifts : Flag())
| (data.is_sell_gifts() ? Flag::SellGifts : Flag())
| (data.is_change_gift_settings() ? Flag::GiftSettings : Flag())
| (data.is_transfer_and_upgrade_gifts() ? Flag::TransferGifts : Flag())
| (data.is_transfer_stars() ? Flag::TransferStars : Flag())
| (data.is_manage_stories() ? Flag::ManageStories : Flag());
}
MTPBusinessBotRights ToMTP(ChatbotsPermissions rights) {
using Flag = MTPDbusinessBotRights::Flag;
using Right = ChatbotsPermission;
return MTP_businessBotRights(MTP_flags(Flag()
| ((rights & Right::ReplyToMessages) ? Flag::f_reply : Flag())
| ((rights & Right::MarkAsRead) ? Flag::f_read_messages : Flag())
| ((rights & Right::DeleteSent) ? Flag::f_delete_sent_messages : Flag())
| ((rights & Right::DeleteReceived) ? Flag::f_delete_received_messages : Flag())
| ((rights & Right::EditName) ? Flag::f_edit_name : Flag())
| ((rights & Right::EditBio) ? Flag::f_edit_bio : Flag())
| ((rights & Right::EditUserpic) ? Flag::f_edit_profile_photo : Flag())
| ((rights & Right::EditUsername) ? Flag::f_edit_username : Flag())
| ((rights & Right::ViewGifts) ? Flag::f_view_gifts : Flag())
| ((rights & Right::SellGifts) ? Flag::f_sell_gifts : Flag())
| ((rights & Right::GiftSettings) ? Flag::f_change_gift_settings : Flag())
| ((rights & Right::TransferGifts) ? Flag::f_transfer_and_upgrade_gifts : Flag())
| ((rights & Right::TransferStars) ? Flag::f_transfer_stars : Flag())
| ((rights & Right::ManageStories) ? Flag::f_manage_stories : Flag())));
}
BusinessDetails FromMTP(
not_null<Session*> owner,
const tl::conditional<MTPBusinessWorkHours> &hours,
const tl::conditional<MTPBusinessLocation> &location,
const tl::conditional<MTPBusinessIntro> &intro) {
auto result = BusinessDetails();
if (hours) {
const auto &data = hours->data();
result.hours.timezoneId = qs(data.vtimezone_id());
result.hours.intervals.list = ranges::views::all(
data.vweekly_open().v
) | ranges::views::transform([](const MTPBusinessWeeklyOpen &open) {
const auto &data = open.data();
return WorkingInterval{
data.vstart_minute().v * 60,
data.vend_minute().v * 60,
};
}) | ranges::to_vector;
}
if (location) {
const auto &data = location->data();
result.location.address = qs(data.vaddress());
if (const auto point = data.vgeo_point()) {
point->match([&](const MTPDgeoPoint &data) {
result.location.point = LocationPoint(data);
}, [&](const MTPDgeoPointEmpty &) {
});
}
}
if (intro) {
const auto &data = intro->data();
result.intro.title = qs(data.vtitle());
result.intro.description = qs(data.vdescription());
if (const auto document = data.vsticker()) {
result.intro.sticker = owner->processDocument(*document);
if (!result.intro.sticker->sticker()) {
result.intro.sticker = nullptr;
}
}
}
return result;
}
[[nodiscard]] AwaySettings FromMTP(
not_null<Session*> owner,
const tl::conditional<MTPBusinessAwayMessage> &message) {
if (!message) {
return AwaySettings();
}
const auto &data = message->data();
auto result = AwaySettings{
.recipients = FromMTP(owner, data.vrecipients()),
.shortcutId = data.vshortcut_id().v,
.offlineOnly = data.is_offline_only(),
};
data.vschedule().match([&](
const MTPDbusinessAwayMessageScheduleAlways &) {
result.schedule.type = AwayScheduleType::Always;
}, [&](const MTPDbusinessAwayMessageScheduleOutsideWorkHours &) {
result.schedule.type = AwayScheduleType::OutsideWorkingHours;
}, [&](const MTPDbusinessAwayMessageScheduleCustom &data) {
result.schedule.type = AwayScheduleType::Custom;
result.schedule.customInterval = WorkingInterval{
data.vstart_date().v,
data.vend_date().v,
};
});
return result;
}
[[nodiscard]] GreetingSettings FromMTP(
not_null<Session*> owner,
const tl::conditional<MTPBusinessGreetingMessage> &message) {
if (!message) {
return GreetingSettings();
}
const auto &data = message->data();
return GreetingSettings{
.recipients = FromMTP(owner, data.vrecipients()),
.noActivityDays = data.vno_activity_days().v,
.shortcutId = data.vshortcut_id().v,
};
}
WorkingIntervals WorkingIntervals::normalized() const {
return SortAndMerge(MoveTailToFront(SortAndMerge(*this)));
}
WorkingIntervals ExtractDayIntervals(
const WorkingIntervals &intervals,
int dayIndex) {
Expects(dayIndex >= 0 && dayIndex < 7);
auto result = WorkingIntervals();
auto &list = result.list;
for (const auto &interval : intervals.list) {
const auto now = interval.intersected(
{ (dayIndex - 1) * kDay, (dayIndex + 2) * kDay });
const auto after = interval.intersected(
{ (dayIndex + 6) * kDay, (dayIndex + 9) * kDay });
const auto before = interval.intersected(
{ (dayIndex - 8) * kDay, (dayIndex - 5) * kDay });
if (now) {
list.push_back(now.shifted(-dayIndex * kDay));
}
if (after) {
list.push_back(after.shifted(-(dayIndex + 7) * kDay));
}
if (before) {
list.push_back(before.shifted(-(dayIndex - 7) * kDay));
}
}
result = result.normalized();
const auto outside = [&](WorkingInterval interval) {
return (interval.end <= 0) || (interval.start >= kDay);
};
list.erase(ranges::remove_if(list, outside), end(list));
if (!list.empty() && list.back().start <= 0 && list.back().end >= kDay) {
list.back() = { 0, kDay };
} else if (!list.empty() && (list.back().end > kDay + kInNextDayMax)) {
list.back() = list.back().intersected({ 0, kDay });
}
if (!list.empty() && list.front().start <= 0) {
if (list.front().start < 0
&& list.front().end <= kInNextDayMax
&& list.front().start > -kDay) {
list.erase(begin(list));
} else {
list.front() = list.front().intersected({ 0, kDay });
if (!list.front()) {
list.erase(begin(list));
}
}
}
return result;
}
bool IsFullOpen(const WorkingIntervals &extractedDay) {
return extractedDay // 00:00-23:59 or 00:00-00:00 (next day)
&& (extractedDay.list.front() == WorkingInterval{ 0, kDay - 60 }
|| extractedDay.list.front() == WorkingInterval{ 0, kDay });
}
WorkingIntervals RemoveDayIntervals(
const WorkingIntervals &intervals,
int dayIndex) {
auto result = intervals.normalized();
auto &list = result.list;
const auto day = WorkingInterval{ 0, kDay };
const auto shifted = day.shifted(dayIndex * kDay);
auto before = WorkingInterval{ 0, shifted.start };
auto after = WorkingInterval{ shifted.end, kWeek };
for (auto i = 0, count = int(list.size()); i != count; ++i) {
if (list[i].end <= shifted.start || list[i].start >= shifted.end) {
continue;
} else if (list[i].end <= shifted.start + kInNextDayMax
&& (list[i].start < shifted.start
|| (!dayIndex // This 'Sunday' finishing on next day <= 6:00.
&& list[i].start == shifted.start
&& list.back().end >= kWeek))) {
continue;
} else if (const auto first = list[i].intersected(before)) {
list[i] = first;
if (const auto second = list[i].intersected(after)) {
list.push_back(second);
}
} else if (const auto second = list[i].intersected(after)) {
list[i] = second;
} else {
list.erase(list.begin() + i);
--i;
--count;
}
}
return result.normalized();
}
WorkingIntervals ReplaceDayIntervals(
const WorkingIntervals &intervals,
int dayIndex,
WorkingIntervals replacement) {
auto result = RemoveDayIntervals(intervals, dayIndex);
const auto first = result.list.insert(
end(result.list),
begin(replacement.list),
end(replacement.list));
for (auto &interval : ranges::make_subrange(first, end(result.list))) {
interval = interval.shifted(dayIndex * kDay);
}
return result.normalized();
}
} // namespace Data

View File

@@ -0,0 +1,302 @@
/*
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 "data/data_location.h"
class UserData;
namespace Data {
class Session;
enum class BusinessChatType {
NewChats = (1 << 0),
ExistingChats = (1 << 1),
Contacts = (1 << 2),
NonContacts = (1 << 3),
};
inline constexpr bool is_flag_type(BusinessChatType) { return true; }
using BusinessChatTypes = base::flags<BusinessChatType>;
struct BusinessChats {
BusinessChatTypes types;
std::vector<not_null<UserData*>> list;
[[nodiscard]] bool empty() const {
return !types && list.empty();
}
friend inline bool operator==(
const BusinessChats &a,
const BusinessChats &b) = default;
};
struct BusinessRecipients {
BusinessChats included;
BusinessChats excluded;
bool allButExcluded = false;
[[nodiscard]] static BusinessRecipients MakeValid(
BusinessRecipients value);
friend inline bool operator==(
const BusinessRecipients &a,
const BusinessRecipients &b) = default;
};
enum class BusinessRecipientsType : uchar {
Messages,
Bots,
};
enum class ChatbotsPermission {
ViewMessages = 0x0001,
ReplyToMessages = 0x0002,
MarkAsRead = 0x0004,
DeleteSent = 0x0008,
DeleteReceived = 0x0010,
EditName = 0x0020,
EditBio = 0x0040,
EditUserpic = 0x0080,
EditUsername = 0x0100,
ViewGifts = 0x0200,
SellGifts = 0x0400,
GiftSettings = 0x0800,
TransferGifts = 0x1000,
TransferStars = 0x2000,
ManageStories = 0x4000,
};
inline constexpr bool is_flag_type(ChatbotsPermission) { return true; }
using ChatbotsPermissions = base::flags<ChatbotsPermission>;
[[nodiscard]] MTPInputBusinessRecipients ForMessagesToMTP(
const BusinessRecipients &data);
[[nodiscard]] MTPInputBusinessBotRecipients ForBotsToMTP(
const BusinessRecipients &data);
[[nodiscard]] BusinessRecipients FromMTP(
not_null<Session*> owner,
const MTPBusinessRecipients &recipients);
[[nodiscard]] BusinessRecipients FromMTP(
not_null<Session*> owner,
const MTPBusinessBotRecipients &recipients);
[[nodiscard]] ChatbotsPermissions FromMTP(
const MTPBusinessBotRights &rights);
[[nodiscard]] MTPBusinessBotRights ToMTP(ChatbotsPermissions rights);
struct Timezone {
QString id;
QString name;
TimeId utcOffset = 0;
friend inline bool operator==(
const Timezone &a,
const Timezone &b) = default;
};
struct Timezones {
std::vector<Timezone> list;
friend inline bool operator==(
const Timezones &a,
const Timezones &b) = default;
};;
struct WorkingInterval {
static constexpr auto kDay = 24 * 3600;
static constexpr auto kWeek = 7 * kDay;
static constexpr auto kInNextDayMax = 6 * 3600;
TimeId start = 0;
TimeId end = 0;
explicit operator bool() const {
return start < end;
}
[[nodiscard]] WorkingInterval shifted(TimeId offset) const {
return { start + offset, end + offset };
}
[[nodiscard]] WorkingInterval united(WorkingInterval other) const {
if (!*this) {
return other;
} else if (!other) {
return *this;
}
return {
std::min(start, other.start),
std::max(end, other.end),
};
}
[[nodiscard]] WorkingInterval intersected(WorkingInterval other) const {
const auto result = WorkingInterval{
std::max(start, other.start),
std::min(end, other.end),
};
return result ? result : WorkingInterval();
}
friend inline bool operator==(
const WorkingInterval &a,
const WorkingInterval &b) = default;
};
struct WorkingIntervals {
std::vector<WorkingInterval> list;
[[nodiscard]] WorkingIntervals normalized() const;
explicit operator bool() const {
for (const auto &interval : list) {
if (interval) {
return true;
}
}
return false;
}
friend inline bool operator==(
const WorkingIntervals &a,
const WorkingIntervals &b) = default;
};
struct WorkingHours {
WorkingIntervals intervals;
QString timezoneId;
[[nodiscard]] WorkingHours normalized() const {
return { intervals.normalized(), timezoneId };
}
explicit operator bool() const {
return !timezoneId.isEmpty() && !intervals.list.empty();
}
friend inline bool operator==(
const WorkingHours &a,
const WorkingHours &b) = default;
};
[[nodiscard]] WorkingIntervals ExtractDayIntervals(
const WorkingIntervals &intervals,
int dayIndex);
[[nodiscard]] bool IsFullOpen(const WorkingIntervals &extractedDay);
[[nodiscard]] WorkingIntervals RemoveDayIntervals(
const WorkingIntervals &intervals,
int dayIndex);
[[nodiscard]] WorkingIntervals ReplaceDayIntervals(
const WorkingIntervals &intervals,
int dayIndex,
WorkingIntervals replacement);
struct BusinessLocation {
QString address;
std::optional<LocationPoint> point;
explicit operator bool() const {
return !address.isEmpty();
}
friend inline bool operator==(
const BusinessLocation &a,
const BusinessLocation &b) = default;
};
struct ChatIntro {
QString title;
QString description;
DocumentData *sticker = nullptr;
[[nodiscard]] bool customPhrases() const {
return !title.isEmpty() || !description.isEmpty();
}
explicit operator bool() const {
return customPhrases() || sticker;
}
friend inline bool operator==(
const ChatIntro &a,
const ChatIntro &b) = default;
};
struct BusinessDetails {
WorkingHours hours;
BusinessLocation location;
ChatIntro intro;
explicit operator bool() const {
return hours || location || intro;
}
friend inline bool operator==(
const BusinessDetails &a,
const BusinessDetails &b) = default;
};
[[nodiscard]] BusinessDetails FromMTP(
not_null<Session*> owner,
const tl::conditional<MTPBusinessWorkHours> &hours,
const tl::conditional<MTPBusinessLocation> &location,
const tl::conditional<MTPBusinessIntro> &intro);
enum class AwayScheduleType : uchar {
Never = 0,
Always = 1,
OutsideWorkingHours = 2,
Custom = 3,
};
struct AwaySchedule {
AwayScheduleType type = AwayScheduleType::Never;
WorkingInterval customInterval;
friend inline bool operator==(
const AwaySchedule &a,
const AwaySchedule &b) = default;
};
struct AwaySettings {
BusinessRecipients recipients;
AwaySchedule schedule;
BusinessShortcutId shortcutId = 0;
bool offlineOnly = false;
explicit operator bool() const {
return schedule.type != AwayScheduleType::Never;
}
friend inline bool operator==(
const AwaySettings &a,
const AwaySettings &b) = default;
};
[[nodiscard]] AwaySettings FromMTP(
not_null<Session*> owner,
const tl::conditional<MTPBusinessAwayMessage> &message);
struct GreetingSettings {
BusinessRecipients recipients;
int noActivityDays = 0;
BusinessShortcutId shortcutId = 0;
explicit operator bool() const {
return noActivityDays > 0;
}
friend inline bool operator==(
const GreetingSettings &a,
const GreetingSettings &b) = default;
};
[[nodiscard]] GreetingSettings FromMTP(
not_null<Session*> owner,
const tl::conditional<MTPBusinessGreetingMessage> &message);
} // namespace Data

View File

@@ -0,0 +1,318 @@
/*
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/business/data_business_info.h"
#include "apiwrap.h"
#include "base/unixtime.h"
#include "data/business/data_business_common.h"
#include "data/data_document.h"
#include "data/data_session.h"
#include "data/data_user.h"
#include "main/main_session.h"
namespace Data {
namespace {
[[nodiscard]] MTPBusinessWorkHours ToMTP(const WorkingHours &data) {
const auto list = data.intervals.normalized().list;
const auto proj = [](const WorkingInterval &data) {
return MTPBusinessWeeklyOpen(MTP_businessWeeklyOpen(
MTP_int(data.start / 60),
MTP_int(data.end / 60)));
};
return MTP_businessWorkHours(
MTP_flags(0),
MTP_string(data.timezoneId),
MTP_vector_from_range(list | ranges::views::transform(proj)));
}
[[nodiscard]] MTPBusinessAwayMessageSchedule ToMTP(
const AwaySchedule &data) {
Expects(data.type != AwayScheduleType::Never);
return (data.type == AwayScheduleType::Always)
? MTP_businessAwayMessageScheduleAlways()
: (data.type == AwayScheduleType::OutsideWorkingHours)
? MTP_businessAwayMessageScheduleOutsideWorkHours()
: MTP_businessAwayMessageScheduleCustom(
MTP_int(data.customInterval.start),
MTP_int(data.customInterval.end));
}
[[nodiscard]] MTPInputBusinessAwayMessage ToMTP(const AwaySettings &data) {
using Flag = MTPDinputBusinessAwayMessage::Flag;
return MTP_inputBusinessAwayMessage(
MTP_flags(data.offlineOnly ? Flag::f_offline_only : Flag()),
MTP_int(data.shortcutId),
ToMTP(data.schedule),
ForMessagesToMTP(data.recipients));
}
[[nodiscard]] MTPInputBusinessGreetingMessage ToMTP(
const GreetingSettings &data) {
return MTP_inputBusinessGreetingMessage(
MTP_int(data.shortcutId),
ForMessagesToMTP(data.recipients),
MTP_int(data.noActivityDays));
}
} // namespace
BusinessInfo::BusinessInfo(not_null<Session*> owner)
: _owner(owner) {
}
BusinessInfo::~BusinessInfo() = default;
void BusinessInfo::saveWorkingHours(
WorkingHours data,
Fn<void(QString)> fail) {
const auto session = &_owner->session();
auto details = session->user()->businessDetails();
const auto &was = details.hours;
if (was == data) {
return;
}
using Flag = MTPaccount_UpdateBusinessWorkHours::Flag;
session->api().request(MTPaccount_UpdateBusinessWorkHours(
MTP_flags(data ? Flag::f_business_work_hours : Flag()),
ToMTP(data)
)).fail([=](const MTP::Error &error) {
auto details = session->user()->businessDetails();
details.hours = was;
session->user()->setBusinessDetails(std::move(details));
if (fail) {
fail(error.type());
}
}).send();
details.hours = std::move(data);
session->user()->setBusinessDetails(std::move(details));
}
void BusinessInfo::saveChatIntro(ChatIntro data, Fn<void(QString)> fail) {
const auto session = &_owner->session();
auto details = session->user()->businessDetails();
const auto &was = details.intro;
if (was == data) {
return;
} else {
const auto session = &_owner->session();
using Flag = MTPaccount_UpdateBusinessIntro::Flag;
session->api().request(MTPaccount_UpdateBusinessIntro(
MTP_flags(data ? Flag::f_intro : Flag()),
MTP_inputBusinessIntro(
MTP_flags(data.sticker
? MTPDinputBusinessIntro::Flag::f_sticker
: MTPDinputBusinessIntro::Flag()),
MTP_string(data.title),
MTP_string(data.description),
(data.sticker
? data.sticker->mtpInput()
: MTP_inputDocumentEmpty()))
)).fail([=](const MTP::Error &error) {
auto details = session->user()->businessDetails();
details.intro = was;
session->user()->setBusinessDetails(std::move(details));
if (fail) {
fail(error.type());
}
}).send();
}
details.intro = std::move(data);
session->user()->setBusinessDetails(std::move(details));
}
void BusinessInfo::saveLocation(
BusinessLocation data,
Fn<void(QString)> fail) {
const auto session = &_owner->session();
auto details = session->user()->businessDetails();
const auto &was = details.location;
if (was == data) {
return;
} else {
const auto session = &_owner->session();
using Flag = MTPaccount_UpdateBusinessLocation::Flag;
session->api().request(MTPaccount_UpdateBusinessLocation(
MTP_flags((data.point ? Flag::f_geo_point : Flag())
| (data.address.isEmpty() ? Flag() : Flag::f_address)),
(data.point
? MTP_inputGeoPoint(
MTP_flags(0),
MTP_double(data.point->lat()),
MTP_double(data.point->lon()),
MTPint()) // accuracy_radius
: MTP_inputGeoPointEmpty()),
MTP_string(data.address)
)).fail([=](const MTP::Error &error) {
auto details = session->user()->businessDetails();
details.location = was;
session->user()->setBusinessDetails(std::move(details));
if (fail) {
fail(error.type());
}
}).send();
}
details.location = std::move(data);
session->user()->setBusinessDetails(std::move(details));
}
void BusinessInfo::applyAwaySettings(AwaySettings data) {
if (_awaySettings == data) {
return;
}
_awaySettings = data;
_awaySettingsChanged.fire({});
}
void BusinessInfo::saveAwaySettings(
AwaySettings data,
Fn<void(QString)> fail) {
const auto &was = _awaySettings;
if (was == data) {
return;
} else if (!data || data.shortcutId) {
using Flag = MTPaccount_UpdateBusinessAwayMessage::Flag;
const auto session = &_owner->session();
session->api().request(MTPaccount_UpdateBusinessAwayMessage(
MTP_flags(data ? Flag::f_message : Flag()),
data ? ToMTP(data) : MTPInputBusinessAwayMessage()
)).fail([=](const MTP::Error &error) {
_awaySettings = was;
_awaySettingsChanged.fire({});
if (fail) {
fail(error.type());
}
}).send();
}
_awaySettings = std::move(data);
_awaySettingsChanged.fire({});
}
bool BusinessInfo::awaySettingsLoaded() const {
return _awaySettings.has_value();
}
AwaySettings BusinessInfo::awaySettings() const {
return _awaySettings.value_or(AwaySettings());
}
rpl::producer<> BusinessInfo::awaySettingsChanged() const {
return _awaySettingsChanged.events();
}
void BusinessInfo::applyGreetingSettings(GreetingSettings data) {
if (_greetingSettings == data) {
return;
}
_greetingSettings = data;
_greetingSettingsChanged.fire({});
}
void BusinessInfo::saveGreetingSettings(
GreetingSettings data,
Fn<void(QString)> fail) {
const auto &was = _greetingSettings;
if (was == data) {
return;
} else if (!data || data.shortcutId) {
using Flag = MTPaccount_UpdateBusinessGreetingMessage::Flag;
_owner->session().api().request(
MTPaccount_UpdateBusinessGreetingMessage(
MTP_flags(data ? Flag::f_message : Flag()),
data ? ToMTP(data) : MTPInputBusinessGreetingMessage())
).fail([=](const MTP::Error &error) {
_greetingSettings = was;
_greetingSettingsChanged.fire({});
if (fail) {
fail(error.type());
}
}).send();
}
_greetingSettings = std::move(data);
_greetingSettingsChanged.fire({});
}
bool BusinessInfo::greetingSettingsLoaded() const {
return _greetingSettings.has_value();
}
GreetingSettings BusinessInfo::greetingSettings() const {
return _greetingSettings.value_or(GreetingSettings());
}
rpl::producer<> BusinessInfo::greetingSettingsChanged() const {
return _greetingSettingsChanged.events();
}
void BusinessInfo::preload() {
preloadTimezones();
}
void BusinessInfo::preloadTimezones() {
if (!_timezones.current().list.empty() || _timezonesRequestId) {
return;
}
_timezonesRequestId = _owner->session().api().request(
MTPhelp_GetTimezonesList(MTP_int(_timezonesHash))
).done([=](const MTPhelp_TimezonesList &result) {
result.match([&](const MTPDhelp_timezonesList &data) {
_timezonesHash = data.vhash().v;
const auto proj = [](const MTPtimezone &result) {
return Timezone{
.id = qs(result.data().vid()),
.name = qs(result.data().vname()),
.utcOffset = result.data().vutc_offset().v,
};
};
_timezones = Timezones{
.list = ranges::views::all(
data.vtimezones().v
) | ranges::views::transform(
proj
) | ranges::to_vector,
};
}, [](const MTPDhelp_timezonesListNotModified &) {
});
}).send();
}
rpl::producer<Timezones> BusinessInfo::timezonesValue() const {
const_cast<BusinessInfo*>(this)->preloadTimezones();
return _timezones.value();
}
bool BusinessInfo::timezonesLoaded() const {
return !_timezones.current().list.empty();
}
QString FindClosestTimezoneId(const std::vector<Timezone> &list) {
const auto local = QDateTime::currentDateTime();
const auto utc = QDateTime(local.date(), local.time(), Qt::UTC);
const auto shift = base::unixtime::now() - (TimeId)::time(nullptr);
const auto delta = int(utc.toSecsSinceEpoch())
- int(local.toSecsSinceEpoch())
- shift;
const auto proj = [&](const Timezone &value) {
auto distance = value.utcOffset - delta;
while (distance > 12 * 3600) {
distance -= 24 * 3600;
}
while (distance < -12 * 3600) {
distance += 24 * 3600;
}
return std::abs(distance);
};
return ranges::min_element(list, ranges::less(), proj)->id;
}
} // namespace Data

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/business/data_business_common.h"
namespace Data {
class Session;
class BusinessInfo final {
public:
explicit BusinessInfo(not_null<Session*> owner);
~BusinessInfo();
void preload();
void saveWorkingHours(WorkingHours data, Fn<void(QString)> fail);
void saveChatIntro(ChatIntro data, Fn<void(QString)> fail);
void saveLocation(BusinessLocation data, Fn<void(QString)> fail);
void saveAwaySettings(AwaySettings data, Fn<void(QString)> fail);
void applyAwaySettings(AwaySettings data);
[[nodiscard]] AwaySettings awaySettings() const;
[[nodiscard]] bool awaySettingsLoaded() const;
[[nodiscard]] rpl::producer<> awaySettingsChanged() const;
void saveGreetingSettings(
GreetingSettings data,
Fn<void(QString)> fail);
void applyGreetingSettings(GreetingSettings data);
[[nodiscard]] GreetingSettings greetingSettings() const;
[[nodiscard]] bool greetingSettingsLoaded() const;
[[nodiscard]] rpl::producer<> greetingSettingsChanged() const;
void preloadTimezones();
[[nodiscard]] bool timezonesLoaded() const;
[[nodiscard]] rpl::producer<Timezones> timezonesValue() const;
private:
const not_null<Session*> _owner;
rpl::variable<Timezones> _timezones;
std::optional<AwaySettings> _awaySettings;
rpl::event_stream<> _awaySettingsChanged;
std::optional<GreetingSettings> _greetingSettings;
rpl::event_stream<> _greetingSettingsChanged;
mtpRequestId _timezonesRequestId = 0;
int32 _timezonesHash = 0;
};
[[nodiscard]] QString FindClosestTimezoneId(
const std::vector<Timezone> &list);
} // namespace Data

View File

@@ -0,0 +1,786 @@
/*
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/business/data_shortcut_messages.h"
#include "api/api_hash.h"
#include "apiwrap.h"
#include "base/unixtime.h"
#include "data/data_peer.h"
#include "data/data_session.h"
#include "api/api_text_entities.h"
#include "main/main_session.h"
#include "history/history.h"
#include "history/history_item_components.h"
#include "history/history_item_helpers.h"
#include "apiwrap.h"
namespace Data {
namespace {
constexpr auto kRequestTimeLimit = 60 * crl::time(1000);
[[nodiscard]] MsgId RemoteToLocalMsgId(MsgId id) {
Expects(IsServerMsgId(id));
return ScheduledMaxMsgId + id + 1;
}
[[nodiscard]] MsgId LocalToRemoteMsgId(MsgId id) {
Expects(IsShortcutMsgId(id));
return (id - ScheduledMaxMsgId - 1);
}
[[nodiscard]] bool TooEarlyForRequest(crl::time received) {
return (received > 0) && (received + kRequestTimeLimit > crl::now());
}
[[nodiscard]] MTPMessage PrepareMessage(
BusinessShortcutId shortcutId,
const MTPMessage &message) {
return message.match([&](const MTPDmessageEmpty &data) {
return MTP_messageEmpty(
data.vflags(),
data.vid(),
data.vpeer_id() ? *data.vpeer_id() : MTPPeer());
}, [&](const MTPDmessageService &data) {
return MTP_messageService(
data.vflags(),
data.vid(),
data.vfrom_id() ? *data.vfrom_id() : MTPPeer(),
data.vpeer_id(),
data.vsaved_peer_id() ? *data.vsaved_peer_id() : MTPPeer(),
data.vreply_to() ? *data.vreply_to() : MTPMessageReplyHeader(),
data.vdate(),
data.vaction(),
data.vreactions() ? *data.vreactions() : MTPMessageReactions(),
MTP_int(data.vttl_period().value_or_empty()));
}, [&](const MTPDmessage &data) {
return MTP_message(
MTP_flags(data.vflags().v
| MTPDmessage::Flag::f_quick_reply_shortcut_id),
data.vid(),
data.vfrom_id() ? *data.vfrom_id() : MTPPeer(),
MTPint(), // from_boosts_applied
data.vpeer_id(),
data.vsaved_peer_id() ? *data.vsaved_peer_id() : MTPPeer(),
data.vfwd_from() ? *data.vfwd_from() : MTPMessageFwdHeader(),
MTP_long(data.vvia_bot_id().value_or_empty()),
MTP_long(data.vvia_business_bot_id().value_or_empty()),
data.vreply_to() ? *data.vreply_to() : MTPMessageReplyHeader(),
data.vdate(),
data.vmessage(),
data.vmedia() ? *data.vmedia() : MTPMessageMedia(),
data.vreply_markup() ? *data.vreply_markup() : MTPReplyMarkup(),
(data.ventities()
? *data.ventities()
: MTPVector<MTPMessageEntity>()),
MTP_int(data.vviews().value_or_empty()),
MTP_int(data.vforwards().value_or_empty()),
data.vreplies() ? *data.vreplies() : MTPMessageReplies(),
MTP_int(data.vedit_date().value_or_empty()),
MTP_bytes(data.vpost_author().value_or_empty()),
MTP_long(data.vgrouped_id().value_or_empty()),
MTPMessageReactions(),
MTPVector<MTPRestrictionReason>(),
MTP_int(data.vttl_period().value_or_empty()),
MTP_int(shortcutId),
MTP_long(data.veffect().value_or_empty()),
(data.vfactcheck() ? *data.vfactcheck() : MTPFactCheck()),
MTP_int(data.vreport_delivery_until_date().value_or_empty()),
MTP_long(data.vpaid_message_stars().value_or_empty()),
(data.vsuggested_post()
? *data.vsuggested_post()
: MTPSuggestedPost()),
MTP_int(data.vschedule_repeat_period().value_or_empty()));
});
}
} // namespace
bool IsShortcutMsgId(MsgId id) {
return (id > ScheduledMaxMsgId) && (id < ShortcutMaxMsgId);
}
ShortcutMessages::ShortcutMessages(not_null<Session*> owner)
: _session(&owner->session())
, _history(owner->history(_session->userPeerId()))
, _clearTimer([=] { clearOldRequests(); }) {
owner->itemRemoved(
) | rpl::filter([](not_null<const HistoryItem*> item) {
return item->isBusinessShortcut();
}) | rpl::on_next([=](not_null<const HistoryItem*> item) {
remove(item);
}, _lifetime);
}
ShortcutMessages::~ShortcutMessages() {
for (const auto &request : _requests) {
_session->api().request(request.second.requestId).cancel();
}
}
void ShortcutMessages::clearOldRequests() {
const auto now = crl::now();
while (true) {
const auto i = ranges::find_if(_requests, [&](const auto &value) {
const auto &request = value.second;
return !request.requestId
&& (request.lastReceived + kRequestTimeLimit <= now);
});
if (i == end(_requests)) {
break;
}
_requests.erase(i);
}
}
void ShortcutMessages::updateShortcuts(const QVector<MTPQuickReply> &list) {
auto shortcuts = parseShortcuts(list);
auto changes = std::vector<ShortcutIdChange>();
for (auto &[id, shortcut] : _shortcuts.list) {
if (shortcuts.list.contains(id)) {
continue;
}
auto foundId = BusinessShortcutId();
for (auto &[realId, real] : shortcuts.list) {
if (real.name == shortcut.name) {
foundId = realId;
break;
}
}
if (foundId) {
mergeMessagesFromTo(id, foundId);
changes.push_back({ .oldId = id, .newId = foundId });
} else {
shortcuts.list.emplace(id, shortcut);
}
}
const auto changed = !_shortcutsLoaded
|| (shortcuts != _shortcuts);
if (changed) {
_shortcuts = std::move(shortcuts);
_shortcutsLoaded = true;
for (const auto &change : changes) {
_shortcutIdChanges.fire_copy(change);
}
_shortcutsChanged.fire({});
} else {
Assert(changes.empty());
}
}
void ShortcutMessages::mergeMessagesFromTo(
BusinessShortcutId fromId,
BusinessShortcutId toId) {
auto &to = _data[toId];
const auto i = _data.find(fromId);
if (i == end(_data)) {
return;
}
auto &from = i->second;
auto destroy = base::flat_set<not_null<HistoryItem*>>();
for (auto &item : from.items) {
if (item->isSending() || item->hasFailed()) {
item->setRealShortcutId(toId);
to.items.push_back(std::move(item));
} else {
destroy.emplace(item.get());
}
}
for (const auto &item : destroy) {
item->destroy();
}
_data.remove(fromId);
cancelRequest(fromId);
_updates.fire_copy(toId);
if (!destroy.empty()) {
cancelRequest(toId);
request(toId);
}
}
Shortcuts ShortcutMessages::parseShortcuts(
const QVector<MTPQuickReply> &list) const {
auto result = Shortcuts();
for (const auto &reply : list) {
const auto shortcut = parseShortcut(reply);
result.list.emplace(shortcut.id, shortcut);
}
return result;
}
Shortcut ShortcutMessages::parseShortcut(const MTPQuickReply &reply) const {
const auto &data = reply.data();
return Shortcut{
.id = BusinessShortcutId(data.vshortcut_id().v),
.count = data.vcount().v,
.name = qs(data.vshortcut()),
.topMessageId = localMessageId(data.vtop_message().v),
};
}
MsgId ShortcutMessages::localMessageId(MsgId remoteId) const {
return RemoteToLocalMsgId(remoteId);
}
MsgId ShortcutMessages::lookupId(not_null<const HistoryItem*> item) const {
Expects(item->isBusinessShortcut());
Expects(!item->isSending());
Expects(!item->hasFailed());
return LocalToRemoteMsgId(item->id);
}
int ShortcutMessages::count(BusinessShortcutId shortcutId) const {
const auto i = _data.find(shortcutId);
return (i != end(_data)) ? i->second.items.size() : 0;
}
void ShortcutMessages::apply(const MTPDupdateQuickReplies &update) {
updateShortcuts(update.vquick_replies().v);
scheduleShortcutsReload();
}
void ShortcutMessages::scheduleShortcutsReload() {
const auto hasUnknownMessages = [&] {
const auto selfId = _session->userPeerId();
for (const auto &[id, shortcut] : _shortcuts.list) {
if (!_session->data().message({ selfId, shortcut.topMessageId })) {
return true;
}
}
return false;
};
if (hasUnknownMessages()) {
_shortcutsLoaded = false;
const auto cancelledId = base::take(_shortcutsRequestId);
_session->api().request(cancelledId).cancel();
crl::on_main(_session, [=] {
if (cancelledId || hasUnknownMessages()) {
preloadShortcuts();
}
});
}
}
void ShortcutMessages::apply(const MTPDupdateNewQuickReply &update) {
const auto &reply = update.vquick_reply();
auto foundId = BusinessShortcutId();
const auto shortcut = parseShortcut(reply);
for (auto &[id, existing] : _shortcuts.list) {
if (id == shortcut.id) {
foundId = id;
break;
} else if (existing.name == shortcut.name) {
foundId = id;
break;
}
}
if (foundId == shortcut.id) {
auto &already = _shortcuts.list[shortcut.id];
if (already != shortcut) {
already = shortcut;
_shortcutsChanged.fire({});
}
return;
} else if (foundId) {
_shortcuts.list.emplace(shortcut.id, shortcut);
mergeMessagesFromTo(foundId, shortcut.id);
_shortcuts.list.remove(foundId);
_shortcutIdChanges.fire({ foundId, shortcut.id });
_shortcutsChanged.fire({});
}
}
void ShortcutMessages::apply(const MTPDupdateQuickReplyMessage &update) {
const auto &message = update.vmessage();
const auto shortcutId = BusinessShortcutIdFromMessage(message);
if (!shortcutId) {
return;
}
const auto loaded = _data.contains(shortcutId);
auto &list = _data[shortcutId];
append(shortcutId, list, message);
sort(list);
_updates.fire_copy(shortcutId);
updateCount(shortcutId);
if (!loaded) {
request(shortcutId);
}
}
void ShortcutMessages::updateCount(BusinessShortcutId shortcutId) {
const auto i = _data.find(shortcutId);
const auto j = _shortcuts.list.find(shortcutId);
if (j == end(_shortcuts.list)) {
return;
}
const auto count = (i != end(_data))
? int(i->second.itemById.size())
: 0;
if (j->second.count != count) {
_shortcuts.list[shortcutId].count = count;
_shortcutsChanged.fire({});
}
}
void ShortcutMessages::apply(
const MTPDupdateDeleteQuickReplyMessages &update) {
const auto shortcutId = update.vshortcut_id().v;
if (!shortcutId) {
return;
}
auto i = _data.find(shortcutId);
if (i == end(_data)) {
return;
}
for (const auto &id : update.vmessages().v) {
const auto &list = i->second;
const auto j = list.itemById.find(id.v);
if (j != end(list.itemById)) {
j->second->destroy();
i = _data.find(shortcutId);
if (i == end(_data)) {
break;
}
}
}
_updates.fire_copy(shortcutId);
updateCount(shortcutId);
cancelRequest(shortcutId);
request(shortcutId);
}
void ShortcutMessages::apply(const MTPDupdateDeleteQuickReply &update) {
const auto shortcutId = update.vshortcut_id().v;
if (!shortcutId) {
return;
}
auto i = _data.find(shortcutId);
while (i != end(_data) && !i->second.itemById.empty()) {
i->second.itemById.back().second->destroy();
i = _data.find(shortcutId);
}
_updates.fire_copy(shortcutId);
if (_data.contains(shortcutId)) {
updateCount(shortcutId);
} else {
_shortcuts.list.remove(shortcutId);
_shortcutIdChanges.fire({ shortcutId, 0 });
}
}
void ShortcutMessages::apply(
const MTPDupdateMessageID &update,
not_null<HistoryItem*> local) {
const auto id = update.vid().v;
const auto i = _data.find(local->shortcutId());
Assert(i != end(_data));
auto &list = i->second;
const auto j = list.itemById.find(id);
if (j != end(list.itemById) || !IsServerMsgId(id)) {
local->destroy();
} else {
Assert(!list.itemById.contains(local->id));
local->setRealId(localMessageId(id));
list.itemById.emplace(id, local);
}
}
void ShortcutMessages::appendSending(not_null<HistoryItem*> item) {
Expects(item->isSending());
Expects(item->isBusinessShortcut());
const auto shortcutId = item->shortcutId();
auto &list = _data[shortcutId];
list.items.emplace_back(item);
sort(list);
_updates.fire_copy(shortcutId);
}
void ShortcutMessages::removeSending(not_null<HistoryItem*> item) {
Expects(item->isSending() || item->hasFailed());
Expects(item->isBusinessShortcut());
item->destroy();
}
rpl::producer<> ShortcutMessages::updates(BusinessShortcutId shortcutId) {
request(shortcutId);
return _updates.events(
) | rpl::filter([=](BusinessShortcutId value) {
return (value == shortcutId);
}) | rpl::to_empty;
}
Data::MessagesSlice ShortcutMessages::list(BusinessShortcutId shortcutId) {
auto result = Data::MessagesSlice();
const auto i = _data.find(shortcutId);
if (i == end(_data)) {
const auto i = _requests.find(shortcutId);
if (i == end(_requests)) {
return result;
}
result.fullCount = result.skippedAfter = result.skippedBefore = 0;
return result;
}
const auto &list = i->second.items;
result.skippedAfter = result.skippedBefore = 0;
result.fullCount = int(list.size());
result.ids = ranges::views::all(
list
) | ranges::views::transform(
&HistoryItem::fullId
) | ranges::to_vector;
return result;
}
void ShortcutMessages::preloadShortcuts() {
if (_shortcutsLoaded || _shortcutsRequestId) {
return;
}
const auto owner = &_session->data();
_shortcutsRequestId = owner->session().api().request(
MTPmessages_GetQuickReplies(MTP_long(_shortcutsHash))
).done([=](const MTPmessages_QuickReplies &result) {
result.match([&](const MTPDmessages_quickReplies &data) {
owner->processUsers(data.vusers());
owner->processChats(data.vchats());
updateShortcuts(data.vquick_replies().v);
}, [&](const MTPDmessages_quickRepliesNotModified &) {
if (!_shortcutsLoaded) {
_shortcutsLoaded = true;
_shortcutsChanged.fire({});
}
});
}).send();
}
const Shortcuts &ShortcutMessages::shortcuts() const {
return _shortcuts;
}
bool ShortcutMessages::shortcutsLoaded() const {
return _shortcutsLoaded;
}
rpl::producer<> ShortcutMessages::shortcutsChanged() const {
return _shortcutsChanged.events();
}
auto ShortcutMessages::shortcutIdChanged() const
-> rpl::producer<ShortcutIdChange> {
return _shortcutIdChanges.events();
}
BusinessShortcutId ShortcutMessages::emplaceShortcut(QString name) {
Expects(_shortcutsLoaded);
for (auto &[id, shortcut] : _shortcuts.list) {
if (shortcut.name == name) {
return id;
}
}
const auto result = --_localShortcutId;
_shortcuts.list.emplace(result, Shortcut{ .id = result, .name = name });
return result;
}
Shortcut ShortcutMessages::lookupShortcut(BusinessShortcutId id) const {
const auto i = _shortcuts.list.find(id);
Ensures(i != end(_shortcuts.list));
return i->second;
}
BusinessShortcutId ShortcutMessages::lookupShortcutId(
const QString &name) const {
for (const auto &[id, shortcut] : _shortcuts.list) {
if (!shortcut.name.compare(name, Qt::CaseInsensitive)) {
return id;
}
}
return {};
}
void ShortcutMessages::editShortcut(
BusinessShortcutId id,
QString name,
Fn<void()> done,
Fn<void(QString)> fail) {
name = name.trimmed();
if (name.isEmpty()) {
fail(QString());
return;
}
const auto finish = [=] {
const auto i = _shortcuts.list.find(id);
if (i != end(_shortcuts.list)) {
i->second.name = name;
_shortcutsChanged.fire({});
}
done();
};
for (const auto &[existingId, shortcut] : _shortcuts.list) {
if (shortcut.name == name) {
if (existingId == id) {
//done();
//return;
break;
} else if (_data[existingId].items.empty() && !shortcut.count) {
removeShortcut(existingId);
break;
} else {
fail(u"SHORTCUT_OCCUPIED"_q);
return;
}
}
}
_session->api().request(MTPmessages_EditQuickReplyShortcut(
MTP_int(id),
MTP_string(name)
)).done(finish).fail([=](const MTP::Error &error) {
const auto type = error.type();
if (type == u"SHORTCUT_ID_INVALID"_q) {
// Not on the server (yet).
finish();
} else {
fail(type);
}
}).send();
}
void ShortcutMessages::removeShortcut(BusinessShortcutId shortcutId) {
auto i = _data.find(shortcutId);
while (i != end(_data)) {
if (i->second.items.empty()) {
_data.erase(i);
} else {
i->second.items.front()->destroy();
}
i = _data.find(shortcutId);
}
_shortcuts.list.remove(shortcutId);
_shortcutIdChanges.fire({ shortcutId, 0 });
_session->api().request(MTPmessages_DeleteQuickReplyShortcut(
MTP_int(shortcutId)
)).send();
}
void ShortcutMessages::cancelRequest(BusinessShortcutId shortcutId) {
const auto j = _requests.find(shortcutId);
if (j != end(_requests)) {
_session->api().request(j->second.requestId).cancel();
_requests.erase(j);
}
}
void ShortcutMessages::request(BusinessShortcutId shortcutId) {
auto &request = _requests[shortcutId];
if (request.requestId || TooEarlyForRequest(request.lastReceived)) {
return;
}
const auto i = _data.find(shortcutId);
const auto hash = (i != end(_data))
? countListHash(i->second)
: uint64(0);
request.requestId = _session->api().request(
MTPmessages_GetQuickReplyMessages(
MTP_flags(0),
MTP_int(shortcutId),
MTPVector<MTPint>(),
MTP_long(hash))
).done([=](const MTPmessages_Messages &result) {
parse(shortcutId, result);
}).fail([=] {
_requests.remove(shortcutId);
}).send();
}
void ShortcutMessages::parse(
BusinessShortcutId shortcutId,
const MTPmessages_Messages &list) {
auto &request = _requests[shortcutId];
request.lastReceived = crl::now();
request.requestId = 0;
if (!_clearTimer.isActive()) {
_clearTimer.callOnce(kRequestTimeLimit * 2);
}
list.match([&](const MTPDmessages_messagesNotModified &data) {
}, [&](const auto &data) {
_session->data().processUsers(data.vusers());
_session->data().processChats(data.vchats());
const auto &messages = data.vmessages().v;
if (messages.isEmpty()) {
clearNotSending(shortcutId);
return;
}
auto received = base::flat_set<not_null<HistoryItem*>>();
auto clear = base::flat_set<not_null<HistoryItem*>>();
auto &list = _data.emplace(shortcutId, List()).first->second;
for (const auto &message : messages) {
if (const auto item = append(shortcutId, list, message)) {
received.emplace(item);
}
}
for (const auto &owned : list.items) {
const auto item = owned.get();
if (!item->isSending() && !received.contains(item)) {
clear.emplace(item);
}
}
updated(shortcutId, received, clear);
});
}
HistoryItem *ShortcutMessages::append(
BusinessShortcutId shortcutId,
List &list,
const MTPMessage &message) {
const auto id = message.match([&](const auto &data) {
return data.vid().v;
});
const auto i = list.itemById.find(id);
if (i != end(list.itemById)) {
const auto existing = i->second;
message.match([&](const MTPDmessage &data) {
if (data.is_edit_hide()) {
existing->applyEdition(HistoryMessageEdition(_session, data));
} else {
existing->updateSentContent({
qs(data.vmessage()),
Api::EntitiesFromMTP(
_session,
data.ventities().value_or_empty())
}, data.vmedia());
existing->updateReplyMarkup(
HistoryMessageMarkupData(data.vreply_markup()));
existing->updateForwardedInfo(data.vfwd_from());
}
existing->updateDate(data.vdate().v);
_history->owner().requestItemTextRefresh(existing);
}, [&](const auto &data) {});
return existing;
}
if (!IsServerMsgId(id)) {
LOG(("API Error: Bad id in quick reply messages: %1.").arg(id));
return nullptr;
}
const auto item = _session->data().addNewMessage(
localMessageId(id),
PrepareMessage(shortcutId, message),
MessageFlags(), // localFlags
NewMessageType::Existing);
if (!item
|| item->history() != _history
|| item->shortcutId() != shortcutId) {
LOG(("API Error: Bad data received in quick reply messages."));
return nullptr;
}
list.items.emplace_back(item);
list.itemById.emplace(id, item);
return item;
}
void ShortcutMessages::clearNotSending(BusinessShortcutId shortcutId) {
const auto i = _data.find(shortcutId);
if (i == end(_data)) {
return;
}
auto clear = base::flat_set<not_null<HistoryItem*>>();
for (const auto &owned : i->second.items) {
if (!owned->isSending() && !owned->hasFailed()) {
clear.emplace(owned.get());
}
}
updated(shortcutId, {}, clear);
}
void ShortcutMessages::updated(
BusinessShortcutId shortcutId,
const base::flat_set<not_null<HistoryItem*>> &added,
const base::flat_set<not_null<HistoryItem*>> &clear) {
if (!clear.empty()) {
for (const auto &item : clear) {
item->destroy();
}
}
const auto i = _data.find(shortcutId);
if (i != end(_data)) {
sort(i->second);
}
if (!added.empty() || !clear.empty()) {
_updates.fire_copy(shortcutId);
}
}
void ShortcutMessages::sort(List &list) {
ranges::sort(list.items, ranges::less(), &HistoryItem::position);
}
void ShortcutMessages::remove(not_null<const HistoryItem*> item) {
const auto shortcutId = item->shortcutId();
const auto i = _data.find(shortcutId);
Assert(i != end(_data));
auto &list = i->second;
if (!item->isSending() && !item->hasFailed()) {
list.itemById.remove(lookupId(item));
}
const auto k = ranges::find(list.items, item, &OwnedItem::get);
Assert(k != list.items.end());
k->release();
list.items.erase(k);
if (list.items.empty()) {
_data.erase(i);
}
_updates.fire_copy(shortcutId);
updateCount(shortcutId);
}
uint64 ShortcutMessages::countListHash(const List &list) const {
using namespace Api;
auto hash = HashInit();
auto &&serverside = ranges::views::all(
list.items
) | ranges::views::filter([](const OwnedItem &item) {
return !item->isSending() && !item->hasFailed();
}) | ranges::views::reverse;
for (const auto &item : serverside) {
HashUpdate(hash, lookupId(item.get()).bare);
if (const auto edited = item->Get<HistoryMessageEdited>()) {
HashUpdate(hash, edited->date);
} else {
HashUpdate(hash, TimeId(0));
}
}
return HashFinalize(hash);
}
MTPInputQuickReplyShortcut ShortcutIdToMTP(
not_null<Main::Session*> session,
BusinessShortcutId id) {
return id
? MTP_inputQuickReplyShortcut(MTP_string(
session->data().shortcutMessages().lookupShortcut(id).name))
: MTPInputQuickReplyShortcut();
}
} // namespace Data

View File

@@ -0,0 +1,154 @@
/*
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 "history/history_item.h"
#include "base/timer.h"
class History;
namespace Main {
class Session;
} // namespace Main
namespace Data {
class Session;
struct MessagesSlice;
struct Shortcut {
BusinessShortcutId id = 0;
int count = 0;
QString name;
MsgId topMessageId = 0;
friend inline bool operator==(
const Shortcut &a,
const Shortcut &b) = default;
};
struct ShortcutIdChange {
BusinessShortcutId oldId = 0;
BusinessShortcutId newId = 0;
};
struct Shortcuts {
base::flat_map<BusinessShortcutId, Shortcut> list;
friend inline bool operator==(
const Shortcuts &a,
const Shortcuts &b) = default;
};
[[nodiscard]] bool IsShortcutMsgId(MsgId id);
class ShortcutMessages final {
public:
explicit ShortcutMessages(not_null<Session*> owner);
~ShortcutMessages();
[[nodiscard]] MsgId lookupId(not_null<const HistoryItem*> item) const;
[[nodiscard]] int count(BusinessShortcutId shortcutId) const;
[[nodiscard]] MsgId localMessageId(MsgId remoteId) const;
void apply(const MTPDupdateQuickReplies &update);
void apply(const MTPDupdateNewQuickReply &update);
void apply(const MTPDupdateQuickReplyMessage &update);
void apply(const MTPDupdateDeleteQuickReplyMessages &update);
void apply(const MTPDupdateDeleteQuickReply &update);
void apply(
const MTPDupdateMessageID &update,
not_null<HistoryItem*> local);
void appendSending(not_null<HistoryItem*> item);
void removeSending(not_null<HistoryItem*> item);
[[nodiscard]] rpl::producer<> updates(BusinessShortcutId shortcutId);
[[nodiscard]] Data::MessagesSlice list(BusinessShortcutId shortcutId);
void preloadShortcuts();
[[nodiscard]] const Shortcuts &shortcuts() const;
[[nodiscard]] bool shortcutsLoaded() const;
[[nodiscard]] rpl::producer<> shortcutsChanged() const;
[[nodiscard]] rpl::producer<ShortcutIdChange> shortcutIdChanged() const;
[[nodiscard]] BusinessShortcutId emplaceShortcut(QString name);
[[nodiscard]] Shortcut lookupShortcut(BusinessShortcutId id) const;
[[nodiscard]] BusinessShortcutId lookupShortcutId(
const QString &name) const;
void editShortcut(
BusinessShortcutId id,
QString name,
Fn<void()> done,
Fn<void(QString)> fail);
void removeShortcut(BusinessShortcutId shortcutId);
private:
using OwnedItem = std::unique_ptr<HistoryItem, HistoryItem::Destroyer>;
struct List {
std::vector<OwnedItem> items;
base::flat_map<MsgId, not_null<HistoryItem*>> itemById;
};
struct Request {
mtpRequestId requestId = 0;
crl::time lastReceived = 0;
};
void request(BusinessShortcutId shortcutId);
void parse(
BusinessShortcutId shortcutId,
const MTPmessages_Messages &list);
HistoryItem *append(
BusinessShortcutId shortcutId,
List &list,
const MTPMessage &message);
void clearNotSending(BusinessShortcutId shortcutId);
void updated(
BusinessShortcutId shortcutId,
const base::flat_set<not_null<HistoryItem*>> &added,
const base::flat_set<not_null<HistoryItem*>> &clear);
void sort(List &list);
void remove(not_null<const HistoryItem*> item);
[[nodiscard]] uint64 countListHash(const List &list) const;
void clearOldRequests();
void cancelRequest(BusinessShortcutId shortcutId);
void updateCount(BusinessShortcutId shortcutId);
void scheduleShortcutsReload();
void mergeMessagesFromTo(
BusinessShortcutId fromId,
BusinessShortcutId toId);
void updateShortcuts(const QVector<MTPQuickReply> &list);
[[nodiscard]] Shortcut parseShortcut(const MTPQuickReply &reply) const;
[[nodiscard]] Shortcuts parseShortcuts(
const QVector<MTPQuickReply> &list) const;
const not_null<Main::Session*> _session;
const not_null<History*> _history;
base::Timer _clearTimer;
base::flat_map<BusinessShortcutId, List> _data;
base::flat_map<BusinessShortcutId, Request> _requests;
rpl::event_stream<BusinessShortcutId> _updates;
Shortcuts _shortcuts;
rpl::event_stream<> _shortcutsChanged;
rpl::event_stream<ShortcutIdChange> _shortcutIdChanges;
BusinessShortcutId _localShortcutId = 0;
uint64 _shortcutsHash = 0;
mtpRequestId _shortcutsRequestId = 0;
bool _shortcutsLoaded = false;
rpl::lifetime _lifetime;
};
[[nodiscard]] MTPInputQuickReplyShortcut ShortcutIdToMTP(
not_null<Main::Session*> session,
BusinessShortcutId id);
} // namespace Data