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