support_helper.cpp 19 KB

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