1 /*
2 This file is part of Telegram Desktop,
3 the official desktop application for the Telegram messaging service.
4
5 For license and copyright information please follow this link:
6 https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
7 */
8 #include "support/support_helper.h"
9
10 #include "dialogs/dialogs_key.h"
11 #include "data/data_drafts.h"
12 #include "data/data_user.h"
13 #include "data/data_session.h"
14 #include "data/data_changes.h"
15 #include "api/api_text_entities.h"
16 #include "history/history.h"
17 #include "boxes/abstract_box.h"
18 #include "ui/toast/toast.h"
19 #include "ui/widgets/input_fields.h"
20 #include "ui/chat/attach/attach_prepare.h"
21 #include "ui/text/format_values.h"
22 #include "ui/text/text_entity.h"
23 #include "ui/text/text_options.h"
24 #include "chat_helpers/message_field.h"
25 #include "chat_helpers/emoji_suggestions_widget.h"
26 #include "base/unixtime.h"
27 #include "lang/lang_keys.h"
28 #include "window/window_session_controller.h"
29 #include "storage/storage_media_prepare.h"
30 #include "storage/localimageloader.h"
31 #include "core/sandbox.h"
32 #include "core/application.h"
33 #include "core/core_settings.h"
34 #include "main/main_session.h"
35 #include "apiwrap.h"
36 #include "facades.h"
37 #include "styles/style_layers.h"
38 #include "styles/style_boxes.h"
39
40 namespace Main {
41 class Session;
42 } // namespace Main
43
44 namespace Support {
45 namespace {
46
47 constexpr auto kOccupyFor = TimeId(60);
48 constexpr auto kReoccupyEach = 30 * crl::time(1000);
49 constexpr auto kMaxSupportInfoLength = MaxMessageSize * 4;
50
51 class EditInfoBox : public Ui::BoxContent {
52 public:
53 EditInfoBox(
54 QWidget*,
55 not_null<Window::SessionController*> controller,
56 const TextWithTags &text,
57 Fn<void(TextWithTags, Fn<void(bool success)>)> submit);
58
59 protected:
60 void prepare() override;
61 void setInnerFocus() override;
62
63 private:
64 const not_null<Window::SessionController*> _controller;
65 object_ptr<Ui::InputField> _field = { nullptr };
66 Fn<void(TextWithTags, Fn<void(bool success)>)> _submit;
67
68 };
69
EditInfoBox(QWidget *,not_null<Window::SessionController * > controller,const TextWithTags & text,Fn<void (TextWithTags,Fn<void (bool success)>)> submit)70 EditInfoBox::EditInfoBox(
71 QWidget*,
72 not_null<Window::SessionController*> controller,
73 const TextWithTags &text,
74 Fn<void(TextWithTags, Fn<void(bool success)>)> submit)
75 : _controller(controller)
76 , _field(
77 this,
78 st::supportInfoField,
79 Ui::InputField::Mode::MultiLine,
80 rpl::single(qsl("Support information")), // #TODO hard_lang
81 text)
82 , _submit(std::move(submit)) {
83 _field->setMaxLength(kMaxSupportInfoLength);
84 _field->setSubmitSettings(
85 Core::App().settings().sendSubmitWay());
86 _field->setInstantReplaces(Ui::InstantReplaces::Default());
87 _field->setInstantReplacesEnabled(
88 Core::App().settings().replaceEmojiValue());
89 _field->setMarkdownReplacesEnabled(rpl::single(true));
90 _field->setEditLinkCallback(DefaultEditLinkCallback(controller, _field));
91 }
92
prepare()93 void EditInfoBox::prepare() {
94 setTitle(rpl::single(qsl("Edit support information"))); // #TODO hard_lang
95
96 const auto save = [=] {
97 const auto done = crl::guard(this, [=](bool success) {
98 if (success) {
99 closeBox();
100 } else {
101 _field->showError();
102 }
103 });
104 _submit(_field->getTextWithAppliedMarkdown(), done);
105 };
106 addButton(tr::lng_settings_save(), save);
107 addButton(tr::lng_cancel(), [=] { closeBox(); });
108
109 connect(_field, &Ui::InputField::submitted, save);
110 connect(_field, &Ui::InputField::cancelled, [=] { closeBox(); });
111 Ui::Emoji::SuggestionsController::Init(
112 getDelegate()->outerContainer(),
113 _field,
114 &_controller->session());
115
116 auto cursor = _field->textCursor();
117 cursor.movePosition(QTextCursor::End);
118 _field->setTextCursor(cursor);
119
120 widthValue(
121 ) | rpl::start_with_next([=](int width) {
122 _field->resizeToWidth(
123 width - st::boxPadding.left() - st::boxPadding.right());
124 _field->moveToLeft(st::boxPadding.left(), st::boxPadding.bottom());
125 }, _field->lifetime());
126
127 _field->heightValue(
128 ) | rpl::start_with_next([=](int height) {
129 setDimensions(
130 st::boxWideWidth,
131 st::boxPadding.bottom() + height + st::boxPadding.bottom());
132 }, _field->lifetime());
133 }
134
setInnerFocus()135 void EditInfoBox::setInnerFocus() {
136 _field->setFocusFast();
137 }
138
OccupationTag()139 uint32 OccupationTag() {
140 return uint32(Core::Sandbox::Instance().installationTag() & 0xFFFFFFFF);
141 }
142
NormalizeName(QString name)143 QString NormalizeName(QString name) {
144 return name.replace(':', '_').replace(';', '_');
145 }
146
OccupiedDraft(const QString & normalizedName)147 Data::Draft OccupiedDraft(const QString &normalizedName) {
148 const auto now = base::unixtime::now(), till = now + kOccupyFor;
149 return {
150 TextWithTags{ "t:"
151 + QString::number(till)
152 + ";u:"
153 + QString::number(OccupationTag())
154 + ";n:"
155 + normalizedName },
156 MsgId(0),
157 MessageCursor(),
158 Data::PreviewState::Allowed
159 };
160 }
161
TrackHistoryOccupation(History * history)162 [[nodiscard]] bool TrackHistoryOccupation(History *history) {
163 if (!history) {
164 return false;
165 } else if (const auto user = history->peer->asUser()) {
166 return !user->isBot();
167 }
168 return false;
169 }
170
ParseOccupationTag(History * history)171 uint32 ParseOccupationTag(History *history) {
172 if (!TrackHistoryOccupation(history)) {
173 return 0;
174 }
175 const auto draft = history->cloudDraft();
176 if (!draft) {
177 return 0;
178 }
179 const auto &text = draft->textWithTags.text;
180 const auto parts = QStringView(text).split(';');
181 auto valid = false;
182 auto result = uint32();
183 for (const auto &part : parts) {
184 if (part.startsWith(qstr("t:"))) {
185 if (base::StringViewMid(part, 2).toInt() >= base::unixtime::now()) {
186 valid = true;
187 } else {
188 return 0;
189 }
190 } else if (part.startsWith(qstr("u:"))) {
191 result = base::StringViewMid(part, 2).toUInt();
192 }
193 }
194 return valid ? result : 0;
195 }
196
ParseOccupationName(History * history)197 QString ParseOccupationName(History *history) {
198 if (!TrackHistoryOccupation(history)) {
199 return QString();
200 }
201 const auto draft = history->cloudDraft();
202 if (!draft) {
203 return QString();
204 }
205 const auto &text = draft->textWithTags.text;
206 const auto parts = QStringView(text).split(';');
207 auto valid = false;
208 auto result = QString();
209 for (const auto &part : parts) {
210 if (part.startsWith(qstr("t:"))) {
211 if (base::StringViewMid(part, 2).toInt() >= base::unixtime::now()) {
212 valid = true;
213 } else {
214 return 0;
215 }
216 } else if (part.startsWith(qstr("n:"))) {
217 result = base::StringViewMid(part, 2).toString();
218 }
219 }
220 return valid ? result : QString();
221 }
222
OccupiedBySomeoneTill(History * history)223 TimeId OccupiedBySomeoneTill(History *history) {
224 if (!TrackHistoryOccupation(history)) {
225 return 0;
226 }
227 const auto draft = history->cloudDraft();
228 if (!draft) {
229 return 0;
230 }
231 const auto &text = draft->textWithTags.text;
232 const auto parts = QStringView(text).split(';');
233 auto valid = false;
234 auto result = TimeId();
235 for (const auto &part : parts) {
236 if (part.startsWith(qstr("t:"))) {
237 if (base::StringViewMid(part, 2).toInt() >= base::unixtime::now()) {
238 result = base::StringViewMid(part, 2).toInt();
239 } else {
240 return 0;
241 }
242 } else if (part.startsWith(qstr("u:"))) {
243 if (base::StringViewMid(part, 2).toUInt() != OccupationTag()) {
244 valid = true;
245 } else {
246 return 0;
247 }
248 }
249 }
250 return valid ? result : 0;
251 }
252
253 } // namespace
254
Helper(not_null<Main::Session * > session)255 Helper::Helper(not_null<Main::Session*> session)
256 : _session(session)
257 , _api(&_session->mtp())
258 , _templates(_session)
259 , _reoccupyTimer([=] { reoccupy(); })
__anon121ca1360902null260 , _checkOccupiedTimer([=] { checkOccupiedChats(); }) {
261 _api.request(MTPhelp_GetSupportName(
__anon121ca1360a02(const MTPhelp_SupportName &result) 262 )).done([=](const MTPhelp_SupportName &result) {
263 result.match([&](const MTPDhelp_supportName &data) {
264 setSupportName(qs(data.vname()));
265 });
266 }).fail([=](const MTP::Error &error) {
267 setSupportName(
268 qsl("[rand^")
269 + QString::number(Core::Sandbox::Instance().installationTag())
270 + ']');
271 }).send();
272 }
273
Create(not_null<Main::Session * > session)274 std::unique_ptr<Helper> Helper::Create(not_null<Main::Session*> session) {
275 //return std::make_unique<Helper>(session); AssertIsDebug();
276 const auto valid = session->user()->phone().startsWith(qstr("424"));
277 return valid ? std::make_unique<Helper>(session) : nullptr;
278 }
279
registerWindow(not_null<Window::SessionController * > controller)280 void Helper::registerWindow(not_null<Window::SessionController*> controller) {
281 controller->activeChatValue(
282 ) | rpl::map([](Dialogs::Key key) {
283 const auto history = key.history();
284 return TrackHistoryOccupation(history) ? history : nullptr;
285 }) | rpl::distinct_until_changed(
286 ) | rpl::start_with_next([=](History *history) {
287 updateOccupiedHistory(controller, history);
288 }, controller->lifetime());
289 }
290
cloudDraftChanged(not_null<History * > history)291 void Helper::cloudDraftChanged(not_null<History*> history) {
292 chatOccupiedUpdated(history);
293 if (history != _occupiedHistory) {
294 return;
295 }
296 occupyIfNotYet();
297 }
298
chatOccupiedUpdated(not_null<History * > history)299 void Helper::chatOccupiedUpdated(not_null<History*> history) {
300 if (const auto till = OccupiedBySomeoneTill(history)) {
301 _occupiedChats[history] = till + 2;
302 history->session().changes().historyUpdated(
303 history,
304 Data::HistoryUpdate::Flag::ChatOccupied);
305 checkOccupiedChats();
306 } else if (_occupiedChats.take(history)) {
307 history->session().changes().historyUpdated(
308 history,
309 Data::HistoryUpdate::Flag::ChatOccupied);
310 }
311 }
312
checkOccupiedChats()313 void Helper::checkOccupiedChats() {
314 const auto now = base::unixtime::now();
315 while (!_occupiedChats.empty()) {
316 const auto nearest = ranges::min_element(
317 _occupiedChats,
318 std::less<>(),
319 [](const auto &pair) { return pair.second; });
320 if (nearest->second <= now) {
321 const auto history = nearest->first;
322 _occupiedChats.erase(nearest);
323 history->session().changes().historyUpdated(
324 history,
325 Data::HistoryUpdate::Flag::ChatOccupied);
326 } else {
327 _checkOccupiedTimer.callOnce(
328 (nearest->second - now) * crl::time(1000));
329 return;
330 }
331 }
332 _checkOccupiedTimer.cancel();
333 }
334
updateOccupiedHistory(not_null<Window::SessionController * > controller,History * history)335 void Helper::updateOccupiedHistory(
336 not_null<Window::SessionController*> controller,
337 History *history) {
338 if (isOccupiedByMe(_occupiedHistory)) {
339 _occupiedHistory->clearCloudDraft();
340 _session->api().saveDraftToCloudDelayed(_occupiedHistory);
341 }
342 _occupiedHistory = history;
343 occupyInDraft();
344 }
345
setSupportName(const QString & name)346 void Helper::setSupportName(const QString &name) {
347 _supportName = name;
348 _supportNameNormalized = NormalizeName(name);
349 occupyIfNotYet();
350 }
351
occupyIfNotYet()352 void Helper::occupyIfNotYet() {
353 if (!isOccupiedByMe(_occupiedHistory)) {
354 occupyInDraft();
355 }
356 }
357
occupyInDraft()358 void Helper::occupyInDraft() {
359 if (_occupiedHistory
360 && !isOccupiedBySomeone(_occupiedHistory)
361 && !_supportName.isEmpty()) {
362 const auto draft = OccupiedDraft(_supportNameNormalized);
363 _occupiedHistory->createCloudDraft(&draft);
364 _session->api().saveDraftToCloudDelayed(_occupiedHistory);
365 _reoccupyTimer.callEach(kReoccupyEach);
366 }
367 }
368
reoccupy()369 void Helper::reoccupy() {
370 if (isOccupiedByMe(_occupiedHistory)) {
371 const auto draft = OccupiedDraft(_supportNameNormalized);
372 _occupiedHistory->createCloudDraft(&draft);
373 _session->api().saveDraftToCloudDelayed(_occupiedHistory);
374 }
375 }
376
isOccupiedByMe(History * history) const377 bool Helper::isOccupiedByMe(History *history) const {
378 if (const auto tag = ParseOccupationTag(history)) {
379 return (tag == OccupationTag());
380 }
381 return false;
382 }
383
isOccupiedBySomeone(History * history) const384 bool Helper::isOccupiedBySomeone(History *history) const {
385 if (const auto tag = ParseOccupationTag(history)) {
386 return (tag != OccupationTag());
387 }
388 return false;
389 }
390
refreshInfo(not_null<UserData * > user)391 void Helper::refreshInfo(not_null<UserData*> user) {
392 _api.request(MTPhelp_GetUserInfo(
393 user->inputUser
394 )).done([=](const MTPhelp_UserInfo &result) {
395 applyInfo(user, result);
396 if (const auto controller = _userInfoEditPending.take(user)) {
397 if (const auto strong = controller->get()) {
398 showEditInfoBox(strong, user);
399 }
400 }
401 }).send();
402 }
403
applyInfo(not_null<UserData * > user,const MTPhelp_UserInfo & result)404 void Helper::applyInfo(
405 not_null<UserData*> user,
406 const MTPhelp_UserInfo &result) {
407 const auto notify = [&] {
408 user->session().changes().peerUpdated(
409 user,
410 Data::PeerUpdate::Flag::SupportInfo);
411 };
412 const auto remove = [&] {
413 if (_userInformation.take(user)) {
414 notify();
415 }
416 };
417 result.match([&](const MTPDhelp_userInfo &data) {
418 auto info = UserInfo();
419 info.author = qs(data.vauthor());
420 info.date = data.vdate().v;
421 info.text = TextWithEntities{
422 qs(data.vmessage()),
423 Api::EntitiesFromMTP(&user->session(), data.ventities().v) };
424 if (info.text.empty()) {
425 remove();
426 } else if (_userInformation[user] != info) {
427 _userInformation[user] = info;
428 notify();
429 }
430 }, [&](const MTPDhelp_userInfoEmpty &) {
431 remove();
432 });
433 }
434
infoValue(not_null<UserData * > user) const435 rpl::producer<UserInfo> Helper::infoValue(not_null<UserData*> user) const {
436 return user->session().changes().peerFlagsValue(
437 user,
438 Data::PeerUpdate::Flag::SupportInfo
439 ) | rpl::map([=] {
440 return infoCurrent(user);
441 });
442 }
443
infoLabelValue(not_null<UserData * > user) const444 rpl::producer<QString> Helper::infoLabelValue(
445 not_null<UserData*> user) const {
446 return infoValue(
447 user
448 ) | rpl::map([](const Support::UserInfo &info) {
449 const auto time = Ui::FormatDateTime(
450 base::unixtime::parse(info.date),
451 cDateFormat(),
452 cTimeFormat());
453 return info.author + ", " + time;
454 });
455 }
456
infoTextValue(not_null<UserData * > user) const457 rpl::producer<TextWithEntities> Helper::infoTextValue(
458 not_null<UserData*> user) const {
459 return infoValue(
460 user
461 ) | rpl::map([](const Support::UserInfo &info) {
462 return info.text;
463 });
464 }
465
infoCurrent(not_null<UserData * > user) const466 UserInfo Helper::infoCurrent(not_null<UserData*> user) const {
467 const auto i = _userInformation.find(user);
468 return (i != end(_userInformation)) ? i->second : UserInfo();
469 }
470
editInfo(not_null<Window::SessionController * > controller,not_null<UserData * > user)471 void Helper::editInfo(
472 not_null<Window::SessionController*> controller,
473 not_null<UserData*> user) {
474 if (!_userInfoEditPending.contains(user)) {
475 _userInfoEditPending.emplace(user, controller.get());
476 refreshInfo(user);
477 }
478 }
479
showEditInfoBox(not_null<Window::SessionController * > controller,not_null<UserData * > user)480 void Helper::showEditInfoBox(
481 not_null<Window::SessionController*> controller,
482 not_null<UserData*> user) {
483 const auto info = infoCurrent(user);
484 const auto editData = TextWithTags{
485 info.text.text,
486 TextUtilities::ConvertEntitiesToTextTags(info.text.entities)
487 };
488
489 const auto save = [=](TextWithTags result, Fn<void(bool)> done) {
490 saveInfo(user, TextWithEntities{
491 result.text,
492 TextUtilities::ConvertTextTagsToEntities(result.tags)
493 }, done);
494 };
495 controller->show(
496 Box<EditInfoBox>(controller, editData, save),
497 Ui::LayerOption::KeepOther);
498 }
499
saveInfo(not_null<UserData * > user,TextWithEntities text,Fn<void (bool success)> done)500 void Helper::saveInfo(
501 not_null<UserData*> user,
502 TextWithEntities text,
503 Fn<void(bool success)> done) {
504 const auto i = _userInfoSaving.find(user);
505 if (i != end(_userInfoSaving)) {
506 if (i->second.data == text) {
507 return;
508 } else {
509 i->second.data = text;
510 _api.request(base::take(i->second.requestId)).cancel();
511 }
512 } else {
513 _userInfoSaving.emplace(user, SavingInfo{ text });
514 }
515
516 TextUtilities::PrepareForSending(
517 text,
518 Ui::ItemTextDefaultOptions().flags);
519 TextUtilities::Trim(text);
520
521 const auto entities = Api::EntitiesToMTP(
522 &user->session(),
523 text.entities,
524 Api::ConvertOption::SkipLocal);
525 _userInfoSaving[user].requestId = _api.request(MTPhelp_EditUserInfo(
526 user->inputUser,
527 MTP_string(text.text),
528 entities
529 )).done([=](const MTPhelp_UserInfo &result) {
530 applyInfo(user, result);
531 done(true);
532 }).fail([=](const MTP::Error &error) {
533 done(false);
534 }).send();
535 }
536
templates()537 Templates &Helper::templates() {
538 return _templates;
539 }
540
ChatOccupiedString(not_null<History * > history)541 QString ChatOccupiedString(not_null<History*> history) {
542 const auto hand = QString::fromUtf8("\xe2\x9c\x8b\xef\xb8\x8f");
543 const auto name = ParseOccupationName(history);
544 return (name.isEmpty() || name.startsWith(qstr("[rand^")))
545 ? hand + " chat taken"
546 : hand + ' ' + name + " is here";
547 }
548
InterpretSendPath(not_null<Window::SessionController * > window,const QString & path)549 QString InterpretSendPath(
550 not_null<Window::SessionController*> window,
551 const QString &path) {
552 QFile f(path);
553 if (!f.open(QIODevice::ReadOnly)) {
554 return "App Error: Could not open interpret file: " + path;
555 }
556 const auto content = QString::fromUtf8(f.readAll());
557 f.close();
558 const auto lines = content.split('\n');
559 auto toId = PeerId(0);
560 auto filePath = QString();
561 auto caption = QString();
562 for (const auto &line : lines) {
563 if (line.startsWith(qstr("from: "))) {
564 if (window->session().userId().bare
565 != base::StringViewMid(
566 line,
567 qstr("from: ").size()).toULongLong()) {
568 return "App Error: Wrong current user.";
569 }
570 } else if (line.startsWith(qstr("channel: "))) {
571 const auto channelId = base::StringViewMid(
572 line,
573 qstr("channel: ").size()).toULongLong();
574 toId = peerFromChannel(channelId);
575 } else if (line.startsWith(qstr("file: "))) {
576 const auto path = line.mid(qstr("file: ").size());
577 if (!QFile(path).exists()) {
578 return "App Error: Could not find file with path: " + path;
579 }
580 filePath = path;
581 } else if (line.startsWith(qstr("caption: "))) {
582 caption = line.mid(qstr("caption: ").size());
583 } else if (!caption.isEmpty()) {
584 caption += '\n' + line;
585 } else {
586 return "App Error: Invalid command: " + line;
587 }
588 }
589 const auto history = window->session().data().historyLoaded(toId);
590 if (!history) {
591 return "App Error: Could not find channel with id: "
592 + QString::number(peerToChannel(toId).bare);
593 }
594 Ui::showPeerHistory(history, ShowAtUnreadMsgId);
595 history->session().api().sendFiles(
596 Storage::PrepareMediaList(QStringList(filePath), st::sendMediaPreviewSize),
597 SendMediaType::File,
598 { caption },
599 nullptr,
600 Api::SendAction(history));
601 return QString();
602 }
603
604 } // namespace Support
605