tdesktop/Telegram/SourceFiles/support/support_helper.cpp
Kirsan 9c562931a2 Respect user settings "Send by ..." for:
forward dialog
send file dialog
edit caption dialog
notification replay
schedule messages
new channel dialog
group description edit dialog
create poll dialog
rate call dialog
report bot dialog
support mode
2020-02-11 12:29:34 +04:00

620 lines
16 KiB
C++

/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "support/support_helper.h"
#include "dialogs/dialogs_key.h"
#include "data/data_drafts.h"
#include "data/data_user.h"
#include "data/data_session.h"
#include "api/api_text_entities.h"
#include "history/history.h"
#include "boxes/abstract_box.h"
#include "ui/toast/toast.h"
#include "ui/widgets/input_fields.h"
#include "ui/text/text_entity.h"
#include "ui/text_options.h"
#include "chat_helpers/message_field.h"
#include "chat_helpers/emoji_suggestions_widget.h"
#include "base/unixtime.h"
#include "lang/lang_keys.h"
#include "window/window_session_controller.h"
#include "storage/storage_media_prepare.h"
#include "storage/localimageloader.h"
#include "core/sandbox.h"
#include "main/main_session.h"
#include "observer_peer.h"
#include "apiwrap.h"
#include "facades.h"
#include "styles/style_layers.h"
#include "styles/style_boxes.h"
namespace Main {
class Session;
} // namespace Main
namespace Support {
namespace {
constexpr auto kOccupyFor = TimeId(60);
constexpr auto kReoccupyEach = 30 * crl::time(1000);
constexpr auto kMaxSupportInfoLength = MaxMessageSize * 4;
class EditInfoBox : public Ui::BoxContent {
public:
EditInfoBox(
QWidget*,
not_null<Main::Session*> session,
const TextWithTags &text,
Fn<void(TextWithTags, Fn<void(bool success)>)> submit);
protected:
void prepare() override;
void setInnerFocus() override;
private:
not_null<Main::Session*> _session;
object_ptr<Ui::InputField> _field = { nullptr };
Fn<void(TextWithTags, Fn<void(bool success)>)> _submit;
};
EditInfoBox::EditInfoBox(
QWidget*,
not_null<Main::Session*> session,
const TextWithTags &text,
Fn<void(TextWithTags, Fn<void(bool success)>)> submit)
: _session(session)
, _field(
this,
st::supportInfoField,
Ui::InputField::Mode::MultiLine,
rpl::single(qsl("Support information")), // #TODO hard_lang
text)
, _submit(std::move(submit)) {
_field->setMaxLength(kMaxSupportInfoLength);
_field->setSubmitSettings(session->settings().sendSubmitWay());
_field->setInstantReplaces(Ui::InstantReplaces::Default());
_field->setInstantReplacesEnabled(
session->settings().replaceEmojiValue());
_field->setMarkdownReplacesEnabled(rpl::single(true));
_field->setEditLinkCallback(DefaultEditLinkCallback(session, _field));
}
void EditInfoBox::prepare() {
setTitle(rpl::single(qsl("Edit support information"))); // #TODO hard_lang
const auto save = [=] {
const auto done = crl::guard(this, [=](bool success) {
if (success) {
closeBox();
} else {
_field->showError();
}
});
_submit(_field->getTextWithAppliedMarkdown(), done);
};
addButton(tr::lng_settings_save(), save);
addButton(tr::lng_cancel(), [=] { closeBox(); });
connect(_field, &Ui::InputField::submitted, save);
connect(_field, &Ui::InputField::cancelled, [=] { closeBox(); });
Ui::Emoji::SuggestionsController::Init(
getDelegate()->outerContainer(),
_field,
_session);
auto cursor = _field->textCursor();
cursor.movePosition(QTextCursor::End);
_field->setTextCursor(cursor);
widthValue(
) | rpl::start_with_next([=](int width) {
_field->resizeToWidth(
width - st::boxPadding.left() - st::boxPadding.right());
_field->moveToLeft(st::boxPadding.left(), st::boxPadding.bottom());
}, _field->lifetime());
_field->heightValue(
) | rpl::start_with_next([=](int height) {
setDimensions(
st::boxWideWidth,
st::boxPadding.bottom() + height + st::boxPadding.bottom());
}, _field->lifetime());
}
void EditInfoBox::setInnerFocus() {
_field->setFocusFast();
}
QString FormatDateTime(TimeId value) {
const auto now = QDateTime::currentDateTime();
const auto date = base::unixtime::parse(value);
if (date.date() == now.date()) {
return tr::lng_mediaview_today(
tr::now,
lt_time,
date.time().toString(cTimeFormat()));
} else if (date.date().addDays(1) == now.date()) {
return tr::lng_mediaview_yesterday(
tr::now,
lt_time,
date.time().toString(cTimeFormat()));
} else {
return tr::lng_mediaview_date_time(
tr::now,
lt_date,
date.date().toString(qsl("dd.MM.yy")),
lt_time,
date.time().toString(cTimeFormat()));
}
}
uint32 OccupationTag() {
return uint32(Core::Sandbox::Instance().installationTag() & 0xFFFFFFFF);
}
QString NormalizeName(QString name) {
return name.replace(':', '_').replace(';', '_');
}
Data::Draft OccupiedDraft(const QString &normalizedName) {
const auto now = base::unixtime::now(), till = now + kOccupyFor;
return {
TextWithTags{ "t:"
+ QString::number(till)
+ ";u:"
+ QString::number(OccupationTag())
+ ";n:"
+ normalizedName },
MsgId(0),
MessageCursor(),
false
};
}
[[nodiscard]] bool TrackHistoryOccupation(History *history) {
if (!history) {
return false;
} else if (const auto user = history->peer->asUser()) {
return !user->isBot();
}
return false;
}
uint32 ParseOccupationTag(History *history) {
if (!TrackHistoryOccupation(history)) {
return 0;
}
const auto draft = history->cloudDraft();
if (!draft) {
return 0;
}
const auto &text = draft->textWithTags.text;
#ifndef OS_MAC_OLD
const auto parts = text.splitRef(';');
#else // OS_MAC_OLD
const auto parts = text.split(';');
#endif // OS_MAC_OLD
auto valid = false;
auto result = uint32();
for (const auto &part : parts) {
if (part.startsWith(qstr("t:"))) {
if (part.mid(2).toInt() >= base::unixtime::now()) {
valid = true;
} else {
return 0;
}
} else if (part.startsWith(qstr("u:"))) {
result = part.mid(2).toUInt();
}
}
return valid ? result : 0;
}
QString ParseOccupationName(History *history) {
if (!TrackHistoryOccupation(history)) {
return QString();
}
const auto draft = history->cloudDraft();
if (!draft) {
return QString();
}
const auto &text = draft->textWithTags.text;
#ifndef OS_MAC_OLD
const auto parts = text.splitRef(';');
#else // OS_MAC_OLD
const auto parts = text.split(';');
#endif // OS_MAC_OLD
auto valid = false;
auto result = QString();
for (const auto &part : parts) {
if (part.startsWith(qstr("t:"))) {
if (part.mid(2).toInt() >= base::unixtime::now()) {
valid = true;
} else {
return 0;
}
} else if (part.startsWith(qstr("n:"))) {
#ifndef OS_MAC_OLD
result = part.mid(2).toString();
#else // OS_MAC_OLD
result = part.mid(2);
#endif // OS_MAC_OLD
}
}
return valid ? result : QString();
}
TimeId OccupiedBySomeoneTill(History *history) {
if (!TrackHistoryOccupation(history)) {
return 0;
}
const auto draft = history->cloudDraft();
if (!draft) {
return 0;
}
const auto &text = draft->textWithTags.text;
#ifndef OS_MAC_OLD
const auto parts = text.splitRef(';');
#else // OS_MAC_OLD
const auto parts = text.split(';');
#endif // OS_MAC_OLD
auto valid = false;
auto result = TimeId();
for (const auto &part : parts) {
if (part.startsWith(qstr("t:"))) {
if (part.mid(2).toInt() >= base::unixtime::now()) {
result = part.mid(2).toInt();
} else {
return 0;
}
} else if (part.startsWith(qstr("u:"))) {
if (part.mid(2).toUInt() != OccupationTag()) {
valid = true;
} else {
return 0;
}
}
}
return valid ? result : 0;
}
} // namespace
Helper::Helper(not_null<Main::Session*> session)
: _session(session)
, _api(_session->api().instance())
, _templates(_session)
, _reoccupyTimer([=] { reoccupy(); })
, _checkOccupiedTimer([=] { checkOccupiedChats(); }) {
_api.request(MTPhelp_GetSupportName(
)).done([=](const MTPhelp_SupportName &result) {
result.match([&](const MTPDhelp_supportName &data) {
setSupportName(qs(data.vname()));
});
}).fail([=](const RPCError &error) {
setSupportName(
qsl("[rand^")
+ QString::number(Core::Sandbox::Instance().installationTag())
+ ']');
}).send();
}
std::unique_ptr<Helper> Helper::Create(not_null<Main::Session*> session) {
//return std::make_unique<Helper>(session); AssertIsDebug();
const auto valid = session->user()->phone().startsWith(qstr("424"));
return valid ? std::make_unique<Helper>(session) : nullptr;
}
void Helper::registerWindow(not_null<Window::SessionController*> controller) {
controller->activeChatValue(
) | rpl::map([](Dialogs::Key key) {
const auto history = key.history();
return TrackHistoryOccupation(history) ? history : nullptr;
}) | rpl::distinct_until_changed(
) | rpl::start_with_next([=](History *history) {
updateOccupiedHistory(controller, history);
}, controller->lifetime());
}
void Helper::cloudDraftChanged(not_null<History*> history) {
chatOccupiedUpdated(history);
if (history != _occupiedHistory) {
return;
}
occupyIfNotYet();
}
void Helper::chatOccupiedUpdated(not_null<History*> history) {
if (const auto till = OccupiedBySomeoneTill(history)) {
_occupiedChats[history] = till + 2;
Notify::peerUpdatedDelayed(
history->peer,
Notify::PeerUpdate::Flag::UserOccupiedChanged);
checkOccupiedChats();
} else if (_occupiedChats.take(history)) {
Notify::peerUpdatedDelayed(
history->peer,
Notify::PeerUpdate::Flag::UserOccupiedChanged);
}
}
void Helper::checkOccupiedChats() {
const auto now = base::unixtime::now();
while (!_occupiedChats.empty()) {
const auto nearest = ranges::min_element(
_occupiedChats,
std::less<>(),
[](const auto &pair) { return pair.second; });
if (nearest->second <= now) {
const auto history = nearest->first;
_occupiedChats.erase(nearest);
Notify::peerUpdatedDelayed(
history->peer,
Notify::PeerUpdate::Flag::UserOccupiedChanged);
} else {
_checkOccupiedTimer.callOnce(
(nearest->second - now) * crl::time(1000));
return;
}
}
_checkOccupiedTimer.cancel();
}
void Helper::updateOccupiedHistory(
not_null<Window::SessionController*> controller,
History *history) {
if (isOccupiedByMe(_occupiedHistory)) {
_occupiedHistory->clearCloudDraft();
_session->api().saveDraftToCloudDelayed(_occupiedHistory);
}
_occupiedHistory = history;
occupyInDraft();
}
void Helper::setSupportName(const QString &name) {
_supportName = name;
_supportNameNormalized = NormalizeName(name);
occupyIfNotYet();
}
void Helper::occupyIfNotYet() {
if (!isOccupiedByMe(_occupiedHistory)) {
occupyInDraft();
}
}
void Helper::occupyInDraft() {
if (_occupiedHistory
&& !isOccupiedBySomeone(_occupiedHistory)
&& !_supportName.isEmpty()) {
const auto draft = OccupiedDraft(_supportNameNormalized);
_occupiedHistory->createCloudDraft(&draft);
_session->api().saveDraftToCloudDelayed(_occupiedHistory);
_reoccupyTimer.callEach(kReoccupyEach);
}
}
void Helper::reoccupy() {
if (isOccupiedByMe(_occupiedHistory)) {
const auto draft = OccupiedDraft(_supportNameNormalized);
_occupiedHistory->createCloudDraft(&draft);
_session->api().saveDraftToCloudDelayed(_occupiedHistory);
}
}
bool Helper::isOccupiedByMe(History *history) const {
if (const auto tag = ParseOccupationTag(history)) {
return (tag == OccupationTag());
}
return false;
}
bool Helper::isOccupiedBySomeone(History *history) const {
if (const auto tag = ParseOccupationTag(history)) {
return (tag != OccupationTag());
}
return false;
}
void Helper::refreshInfo(not_null<UserData*> user) {
_api.request(MTPhelp_GetUserInfo(
user->inputUser
)).done([=](const MTPhelp_UserInfo &result) {
applyInfo(user, result);
if (_userInfoEditPending.contains(user)) {
_userInfoEditPending.erase(user);
showEditInfoBox(user);
}
}).send();
}
void Helper::applyInfo(
not_null<UserData*> user,
const MTPhelp_UserInfo &result) {
const auto notify = [&] {
Notify::peerUpdatedDelayed(
user,
Notify::PeerUpdate::Flag::UserSupportInfoChanged);
};
const auto remove = [&] {
if (_userInformation.take(user)) {
notify();
}
};
result.match([&](const MTPDhelp_userInfo &data) {
auto info = UserInfo();
info.author = qs(data.vauthor());
info.date = data.vdate().v;
info.text = TextWithEntities{
qs(data.vmessage()),
Api::EntitiesFromMTP(data.ventities().v) };
if (info.text.empty()) {
remove();
} else if (_userInformation[user] != info) {
_userInformation[user] = info;
notify();
}
}, [&](const MTPDhelp_userInfoEmpty &) {
remove();
});
}
rpl::producer<UserInfo> Helper::infoValue(not_null<UserData*> user) const {
return Notify::PeerUpdateValue(
user,
Notify::PeerUpdate::Flag::UserSupportInfoChanged
) | rpl::map([=] {
return infoCurrent(user);
});
}
rpl::producer<QString> Helper::infoLabelValue(
not_null<UserData*> user) const {
return infoValue(
user
) | rpl::map([](const Support::UserInfo &info) {
return info.author + ", " + FormatDateTime(info.date);
});
}
rpl::producer<TextWithEntities> Helper::infoTextValue(
not_null<UserData*> user) const {
return infoValue(
user
) | rpl::map([](const Support::UserInfo &info) {
return info.text;
});
}
UserInfo Helper::infoCurrent(not_null<UserData*> user) const {
const auto i = _userInformation.find(user);
return (i != end(_userInformation)) ? i->second : UserInfo();
}
void Helper::editInfo(not_null<UserData*> user) {
if (!_userInfoEditPending.contains(user)) {
_userInfoEditPending.emplace(user);
refreshInfo(user);
}
}
void Helper::showEditInfoBox(not_null<UserData*> user) {
const auto info = infoCurrent(user);
const auto editData = TextWithTags{
info.text.text,
TextUtilities::ConvertEntitiesToTextTags(info.text.entities)
};
const auto save = [=](TextWithTags result, Fn<void(bool)> done) {
saveInfo(user, TextWithEntities{
result.text,
TextUtilities::ConvertTextTagsToEntities(result.tags)
}, done);
};
Ui::show(
Box<EditInfoBox>(&user->session(), editData, save),
Ui::LayerOption::KeepOther);
}
void Helper::saveInfo(
not_null<UserData*> user,
TextWithEntities text,
Fn<void(bool success)> done) {
const auto i = _userInfoSaving.find(user);
if (i != end(_userInfoSaving)) {
if (i->second.data == text) {
return;
} else {
i->second.data = text;
_api.request(base::take(i->second.requestId)).cancel();
}
} else {
_userInfoSaving.emplace(user, SavingInfo{ text });
}
TextUtilities::PrepareForSending(
text,
Ui::ItemTextDefaultOptions().flags);
TextUtilities::Trim(text);
const auto entities = Api::EntitiesToMTP(
text.entities,
Api::ConvertOption::SkipLocal);
_userInfoSaving[user].requestId = _api.request(MTPhelp_EditUserInfo(
user->inputUser,
MTP_string(text.text),
entities
)).done([=](const MTPhelp_UserInfo &result) {
applyInfo(user, result);
done(true);
}).fail([=](const RPCError &error) {
done(false);
}).send();
}
Templates &Helper::templates() {
return _templates;
}
QString ChatOccupiedString(not_null<History*> history) {
const auto hand = QString::fromUtf8("\xe2\x9c\x8b\xef\xb8\x8f");
const auto name = ParseOccupationName(history);
return (name.isEmpty() || name.startsWith(qstr("[rand^")))
? hand + " chat taken"
: hand + ' ' + name + " is here";
}
QString InterpretSendPath(const QString &path) {
QFile f(path);
if (!f.open(QIODevice::ReadOnly)) {
return "App Error: Could not open interpret file: " + path;
}
const auto content = QString::fromUtf8(f.readAll());
f.close();
const auto lines = content.split('\n');
auto toId = PeerId(0);
auto filePath = QString();
auto caption = QString();
for (const auto &line : lines) {
if (line.startsWith(qstr("from: "))) {
if (Auth().userId() != line.mid(qstr("from: ").size()).toInt()) {
return "App Error: Wrong current user.";
}
} else if (line.startsWith(qstr("channel: "))) {
const auto channelId = line.mid(qstr("channel: ").size()).toInt();
toId = peerFromChannel(channelId);
} else if (line.startsWith(qstr("file: "))) {
const auto path = line.mid(qstr("file: ").size());
if (!QFile(path).exists()) {
return "App Error: Could not find file with path: " + path;
}
filePath = path;
} else if (line.startsWith(qstr("caption: "))) {
caption = line.mid(qstr("caption: ").size());
} else if (!caption.isEmpty()) {
caption += '\n' + line;
} else {
return "App Error: Invalid command: " + line;
}
}
const auto history = Auth().data().historyLoaded(toId);
if (!history) {
return "App Error: Could not find channel with id: " + QString::number(peerToChannel(toId));
}
Ui::showPeerHistory(history, ShowAtUnreadMsgId);
history->session().api().sendFiles(
Storage::PrepareMediaList(QStringList(filePath), st::sendMediaPreviewSize),
SendMediaType::File,
{ caption },
nullptr,
Api::SendAction(history));
return QString();
}
} // namespace Support