support_templates.cpp 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739
  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_templates.h"
  8. #include "ui/toast/toast.h"
  9. #include "data/data_session.h"
  10. #include "core/shortcuts.h"
  11. #include "main/main_session.h"
  12. #include <QtNetwork/QNetworkAccessManager>
  13. namespace Support {
  14. namespace details {
  15. namespace {
  16. constexpr auto kQueryLimit = 10;
  17. constexpr auto kWeightStep = 1000;
  18. struct Delta {
  19. std::vector<const TemplatesQuestion*> added;
  20. std::vector<const TemplatesQuestion*> changed;
  21. std::vector<const TemplatesQuestion*> removed;
  22. std::map<QString, QStringList> keys;
  23. explicit operator bool() const {
  24. return !added.empty() || !changed.empty() || !removed.empty();
  25. }
  26. };
  27. bool IsTemplatesFile(const QString &file) {
  28. return file.startsWith(u"tl_"_q, Qt::CaseInsensitive)
  29. && file.endsWith(u".txt"_q, Qt::CaseInsensitive);
  30. }
  31. QString NormalizeQuestion(const QString &question) {
  32. auto result = QString();
  33. result.reserve(question.size());
  34. for (const auto &ch : question) {
  35. if (ch.isLetterOrNumber()) {
  36. result.append(ch.toLower());
  37. }
  38. }
  39. return result;
  40. }
  41. QString NormalizeKey(const QString &query) {
  42. return TextUtilities::RemoveAccents(query.trimmed().toLower());
  43. }
  44. struct FileResult {
  45. TemplatesFile result;
  46. QStringList errors;
  47. };
  48. enum class ReadState {
  49. None,
  50. Question,
  51. Keys,
  52. Value,
  53. Url,
  54. };
  55. template <typename StateChange, typename LineCallback>
  56. void ReadByLine(
  57. const QByteArray &blob,
  58. StateChange &&stateChange,
  59. LineCallback &&lineCallback) {
  60. using State = ReadState;
  61. auto state = State::None;
  62. auto hadKeys = false;
  63. auto hadValue = false;
  64. for (const auto &utf : blob.split('\n')) {
  65. const auto line = QString::fromUtf8(utf).trimmed();
  66. const auto match = QRegularExpression(
  67. u"^\\{([A-Z_]+)\\}$"_q
  68. ).match(line);
  69. if (match.hasMatch()) {
  70. const auto token = match.captured(1);
  71. if (state == State::Value) {
  72. hadKeys = hadValue = false;
  73. }
  74. const auto newState = [&] {
  75. if (token == u"VALUE"_q) {
  76. return hadValue ? State::None : State::Value;
  77. } else if (token == u"KEYS"_q) {
  78. return hadKeys ? State::None : State::Keys;
  79. } else if (token == u"QUESTION"_q) {
  80. return State::Question;
  81. } else if (token == u"URL"_q) {
  82. return State::Url;
  83. } else {
  84. return State::None;
  85. }
  86. }();
  87. stateChange(state, newState);
  88. state = newState;
  89. lineCallback(state, line, true);
  90. } else {
  91. if (!line.isEmpty()) {
  92. if (state == State::Value) {
  93. hadValue = true;
  94. } else if (state == State::Keys) {
  95. hadKeys = true;
  96. }
  97. }
  98. lineCallback(state, line, false);
  99. }
  100. }
  101. }
  102. template <typename Callback>
  103. QString ReadByLineGetUrl(const QByteArray &blob, Callback &&callback) {
  104. using State = ReadState;
  105. auto url = QString();
  106. auto question = TemplatesQuestion();
  107. const auto call = [&] {
  108. while (question.value.endsWith('\n')) {
  109. question.value.chop(1);
  110. }
  111. return callback(base::take(question));
  112. };
  113. ReadByLine(blob, [&](State was, State now) {
  114. if (was == State::Value) {
  115. call();
  116. }
  117. }, [&](State state, const QString &line, bool stateChangeLine) {
  118. if (stateChangeLine) {
  119. return;
  120. }
  121. switch (state) {
  122. case State::Keys:
  123. if (!line.isEmpty()) {
  124. question.originalKeys.push_back(line);
  125. if (const auto norm = NormalizeKey(line); !norm.isEmpty()) {
  126. question.normalizedKeys.push_back(norm);
  127. }
  128. }
  129. break;
  130. case State::Value:
  131. if (!question.value.isEmpty()) {
  132. question.value += '\n';
  133. }
  134. question.value += line;
  135. break;
  136. case State::Question:
  137. if (question.question.isEmpty()) {
  138. question.question = line;
  139. }
  140. break;
  141. case State::Url:
  142. if (url.isEmpty()) {
  143. url = line;
  144. }
  145. break;
  146. }
  147. });
  148. call();
  149. return url;
  150. }
  151. FileResult ReadFromBlob(const QByteArray &blob) {
  152. auto result = FileResult();
  153. result.result.url = ReadByLineGetUrl(blob, [&](TemplatesQuestion &&q) {
  154. const auto normalized = NormalizeQuestion(q.question);
  155. if (!normalized.isEmpty()) {
  156. result.result.questions.emplace(normalized, std::move(q));
  157. }
  158. });
  159. return result;
  160. }
  161. FileResult ReadFile(const QString &path) {
  162. QFile f(path);
  163. if (!f.open(QIODevice::ReadOnly)) {
  164. auto result = FileResult();
  165. result.errors.push_back(
  166. u"Couldn't open '%1' for reading!"_q.arg(path));
  167. return result;
  168. }
  169. const auto blob = f.readAll();
  170. f.close();
  171. return ReadFromBlob(blob);
  172. }
  173. void WriteWithOwnUrlAndKeys(
  174. QIODevice &device,
  175. const QByteArray &blob,
  176. const QString &url,
  177. const Delta &delta) {
  178. device.write("{URL}\n");
  179. device.write(url.toUtf8());
  180. device.write("\n\n");
  181. using State = ReadState;
  182. auto question = QString();
  183. auto normalized = QString();
  184. auto ownKeysWritten = false;
  185. ReadByLine(blob, [&](State was, State now) {
  186. if (was == State::Value) {
  187. question = normalized = QString();
  188. }
  189. }, [&](State state, const QString &line, bool stateChangeLine) {
  190. const auto writeLine = [&] {
  191. device.write(line.toUtf8());
  192. device.write("\n", 1);
  193. };
  194. switch (state) {
  195. case State::Keys:
  196. if (stateChangeLine) {
  197. writeLine();
  198. ownKeysWritten = [&] {
  199. if (normalized.isEmpty()) {
  200. return false;
  201. }
  202. const auto i = delta.keys.find(normalized);
  203. if (i == end(delta.keys)) {
  204. return false;
  205. }
  206. device.write(i->second.join('\n').toUtf8());
  207. device.write("\n", 1);
  208. return true;
  209. }();
  210. } else if (!ownKeysWritten) {
  211. writeLine();
  212. }
  213. break;
  214. case State::Value:
  215. writeLine();
  216. break;
  217. case State::Question:
  218. writeLine();
  219. if (!stateChangeLine && question.isEmpty()) {
  220. question = line;
  221. normalized = NormalizeQuestion(line);
  222. }
  223. break;
  224. case State::Url:
  225. break;
  226. }
  227. });
  228. }
  229. struct FilesResult {
  230. TemplatesData result;
  231. TemplatesIndex index;
  232. QStringList errors;
  233. };
  234. FilesResult ReadFiles(const QString &folder) {
  235. auto result = FilesResult();
  236. const auto files = QDir(folder).entryList(QDir::Files);
  237. for (const auto &path : files) {
  238. if (!IsTemplatesFile(path)) {
  239. continue;
  240. }
  241. auto file = ReadFile(folder + '/' + path);
  242. if (!file.result.url.isEmpty() || !file.result.questions.empty()) {
  243. result.result.files[path] = std::move(file.result);
  244. }
  245. result.errors.append(std::move(file.errors));
  246. }
  247. return result;
  248. }
  249. TemplatesIndex ComputeIndex(const TemplatesData &data) {
  250. using Id = TemplatesIndex::Id;
  251. using Term = TemplatesIndex::Term;
  252. auto uniqueFirst = std::map<QChar, base::flat_set<Id>>();
  253. auto uniqueFull = std::map<Id, base::flat_set<Term>>();
  254. const auto pushString = [&](
  255. const Id &id,
  256. const QString &string,
  257. int weight) {
  258. const auto list = TextUtilities::PrepareSearchWords(string);
  259. for (const auto &word : list) {
  260. uniqueFirst[word[0]].emplace(id);
  261. uniqueFull[id].emplace(std::make_pair(word, weight));
  262. }
  263. };
  264. for (const auto &[path, file] : data.files) {
  265. for (const auto &[normalized, question] : file.questions) {
  266. const auto id = std::make_pair(path, normalized);
  267. for (const auto &key : question.normalizedKeys) {
  268. pushString(id, key, kWeightStep * kWeightStep);
  269. }
  270. pushString(id, question.question, kWeightStep);
  271. pushString(id, question.value, 1);
  272. }
  273. }
  274. auto result = TemplatesIndex();
  275. for (const auto &[ch, unique] : uniqueFirst) {
  276. result.first.emplace(ch, unique | ranges::to_vector);
  277. }
  278. for (const auto &[id, unique] : uniqueFull) {
  279. result.full.emplace(id, unique | ranges::to_vector);
  280. }
  281. return result;
  282. }
  283. void ReplaceFileIndex(
  284. TemplatesIndex &result,
  285. TemplatesIndex &&source,
  286. const QString &path) {
  287. for (auto i = begin(result.full); i != end(result.full);) {
  288. if (i->first.first == path) {
  289. i = result.full.erase(i);
  290. } else {
  291. ++i;
  292. }
  293. }
  294. for (auto &[id, list] : source.full) {
  295. result.full.emplace(id, std::move(list));
  296. }
  297. using Id = TemplatesIndex::Id;
  298. for (auto &[ch, list] : result.first) {
  299. auto i = ranges::lower_bound(
  300. list,
  301. std::make_pair(path, QString()));
  302. auto j = std::find_if(i, end(list), [&](const Id &id) {
  303. return id.first != path;
  304. });
  305. list.erase(i, j);
  306. }
  307. for (auto &[ch, list] : source.first) {
  308. auto &to = result.first[ch];
  309. to.insert(
  310. end(to),
  311. std::make_move_iterator(begin(list)),
  312. std::make_move_iterator(end(list)));
  313. ranges::sort(to);
  314. }
  315. }
  316. void MoveKeys(TemplatesFile &to, const TemplatesFile &from) {
  317. const auto &existing = from.questions;
  318. for (auto &[normalized, question] : to.questions) {
  319. if (const auto i = existing.find(normalized); i != end(existing)) {
  320. question.originalKeys = i->second.originalKeys;
  321. question.normalizedKeys = i->second.normalizedKeys;
  322. }
  323. }
  324. }
  325. Delta ComputeDelta(const TemplatesFile &was, const TemplatesFile &now) {
  326. auto result = Delta();
  327. for (const auto &[normalized, question] : now.questions) {
  328. const auto i = was.questions.find(normalized);
  329. if (i == end(was.questions)) {
  330. result.added.push_back(&question);
  331. } else {
  332. result.keys.emplace(normalized, i->second.originalKeys);
  333. if (i->second.value != question.value) {
  334. result.changed.push_back(&question);
  335. }
  336. }
  337. }
  338. for (const auto &[normalized, question] : was.questions) {
  339. if (result.keys.find(normalized) == end(result.keys)) {
  340. result.removed.push_back(&question);
  341. }
  342. }
  343. return result;
  344. }
  345. QString FormatUpdateNotification(const QString &path, const Delta &delta) {
  346. auto result = u"Template file '%1' updated!\n\n"_q.arg(path);
  347. if (!delta.added.empty()) {
  348. result += u"-------- Added --------\n\n"_q;
  349. for (const auto question : delta.added) {
  350. result += u"Q: %1\nK: %2\nA: %3\n\n"_q.arg(
  351. question->question,
  352. question->originalKeys.join(u", "_q),
  353. question->value.trimmed());
  354. }
  355. }
  356. if (!delta.changed.empty()) {
  357. result += u"-------- Modified --------\n\n"_q;
  358. for (const auto question : delta.changed) {
  359. result += u"Q: %1\nA: %2\n\n"_q.arg(
  360. question->question,
  361. question->value.trimmed());
  362. }
  363. }
  364. if (!delta.removed.empty()) {
  365. result += u"-------- Removed --------\n\n"_q;
  366. for (const auto question : delta.removed) {
  367. result += u"Q: %1\n\n"_q.arg(question->question);
  368. }
  369. }
  370. return result;
  371. }
  372. QString UpdateFile(
  373. const QString &path,
  374. const QByteArray &content,
  375. const QString &url,
  376. const Delta &delta) {
  377. auto result = QString();
  378. const auto full = cWorkingDir() + "TEMPLATES/" + path;
  379. const auto old = full + u".old"_q;
  380. QFile(old).remove();
  381. if (QFile(full).copy(old)) {
  382. result += u"(old file saved at '%1')"_q.arg(path + u".old"_q);
  383. QFile f(full);
  384. if (f.open(QIODevice::WriteOnly)) {
  385. WriteWithOwnUrlAndKeys(f, content, url, delta);
  386. } else {
  387. result += u"\n\nError: could not open new file '%1'!"_q.arg(full);
  388. }
  389. } else {
  390. result += u"Error: could not save old file '%1'!"_q.arg(old);
  391. }
  392. return result;
  393. }
  394. int CountMaxKeyLength(const TemplatesData &data) {
  395. auto result = 0;
  396. for (const auto &[path, file] : data.files) {
  397. for (const auto &[normalized, question] : file.questions) {
  398. for (const auto &key : question.normalizedKeys) {
  399. accumulate_max(result, int(key.size()));
  400. }
  401. }
  402. }
  403. return result;
  404. }
  405. } // namespace
  406. } // namespace details
  407. using namespace details;
  408. struct Templates::Updates {
  409. QNetworkAccessManager manager;
  410. std::map<QString, QNetworkReply*> requests;
  411. };
  412. Templates::Templates(not_null<Main::Session*> session) : _session(session) {
  413. load();
  414. Shortcuts::Requests(
  415. ) | rpl::start_with_next([=](not_null<Shortcuts::Request*> request) {
  416. using Command = Shortcuts::Command;
  417. request->check(
  418. Command::SupportReloadTemplates
  419. ) && request->handle([=] {
  420. reload();
  421. return true;
  422. });
  423. }, _lifetime);
  424. }
  425. void Templates::reload() {
  426. _reloadToastSubscription = errors(
  427. ) | rpl::start_with_next([=](QStringList errors) {
  428. Ui::Toast::Show(errors.isEmpty()
  429. ? "Templates reloaded!"
  430. : ("Errors:\n\n" + errors.join("\n\n")));
  431. });
  432. load();
  433. }
  434. void Templates::load() {
  435. if (_reloadAfterRead) {
  436. return;
  437. } else if (_reading || _updates) {
  438. _reloadAfterRead = true;
  439. return;
  440. }
  441. crl::async([=, guard = _reading.make_guard()]() mutable {
  442. auto result = ReadFiles(cWorkingDir() + "TEMPLATES");
  443. result.index = ComputeIndex(result.result);
  444. crl::on_main(std::move(guard), [
  445. =,
  446. result = std::move(result)
  447. ]() mutable {
  448. setData(std::move(result.result));
  449. _index = std::move(result.index);
  450. _errors.fire(std::move(result.errors));
  451. crl::on_main(this, [=] {
  452. if (base::take(_reloadAfterRead)) {
  453. reload();
  454. } else {
  455. update();
  456. }
  457. });
  458. });
  459. });
  460. }
  461. void Templates::setData(TemplatesData &&data) {
  462. _data = std::move(data);
  463. _maxKeyLength = CountMaxKeyLength(_data);
  464. }
  465. void Templates::ensureUpdatesCreated() {
  466. if (_updates) {
  467. return;
  468. }
  469. _updates = std::make_unique<Updates>();
  470. QObject::connect(
  471. &_updates->manager,
  472. &QNetworkAccessManager::finished,
  473. [=](QNetworkReply *reply) { updateRequestFinished(reply); });
  474. }
  475. void Templates::update() {
  476. const auto sendRequest = [&](const QString &path, const QString &url) {
  477. ensureUpdatesCreated();
  478. if (_updates->requests.find(path) != end(_updates->requests)) {
  479. return;
  480. }
  481. _updates->requests.emplace(
  482. path,
  483. _updates->manager.get(QNetworkRequest(url)));
  484. };
  485. for (const auto &[path, file] : _data.files) {
  486. if (!file.url.isEmpty()) {
  487. sendRequest(path, file.url);
  488. }
  489. }
  490. }
  491. void Templates::updateRequestFinished(QNetworkReply *reply) {
  492. reply->deleteLater();
  493. const auto path = [&] {
  494. for (const auto &[file, sent] : _updates->requests) {
  495. if (sent == reply) {
  496. return file;
  497. }
  498. }
  499. return QString();
  500. }();
  501. if (path.isEmpty()) {
  502. return;
  503. }
  504. _updates->requests[path] = nullptr;
  505. if (reply->error() != QNetworkReply::NoError) {
  506. const auto message = (
  507. u"Error: template update failed, url '%1', error %2, %3"_q
  508. ).arg(reply->url().toDisplayString()
  509. ).arg(reply->error()
  510. ).arg(reply->errorString());
  511. _session->data().serviceNotification({ message });
  512. return;
  513. }
  514. LOG(("Got template from url '%1'"
  515. ).arg(reply->url().toDisplayString()));
  516. const auto content = reply->readAll();
  517. crl::async([=, weak = base::make_weak(this)]{
  518. auto result = ReadFromBlob(content);
  519. auto one = TemplatesData();
  520. one.files.emplace(path, std::move(result.result));
  521. auto index = ComputeIndex(one);
  522. crl::on_main(weak,[
  523. =,
  524. one = std::move(one),
  525. errors = std::move(result.errors),
  526. index = std::move(index)
  527. ]() mutable {
  528. auto &existing = _data.files.at(path);
  529. auto &parsed = one.files.at(path);
  530. MoveKeys(parsed, existing);
  531. ReplaceFileIndex(_index, ComputeIndex(one), path);
  532. if (!errors.isEmpty()) {
  533. _errors.fire(std::move(errors));
  534. }
  535. if (const auto delta = ComputeDelta(existing, parsed)) {
  536. const auto text = FormatUpdateNotification(
  537. path,
  538. delta);
  539. const auto copy = UpdateFile(
  540. path,
  541. content,
  542. existing.url,
  543. delta);
  544. const auto full = text + copy;
  545. _session->data().serviceNotification({ full });
  546. }
  547. _data.files.at(path) = std::move(one.files.at(path));
  548. _updates->requests.erase(path);
  549. checkUpdateFinished();
  550. });
  551. });
  552. }
  553. void Templates::checkUpdateFinished() {
  554. if (!_updates || !_updates->requests.empty()) {
  555. return;
  556. }
  557. _updates = nullptr;
  558. if (base::take(_reloadAfterRead)) {
  559. reload();
  560. }
  561. }
  562. auto Templates::matchExact(QString query) const
  563. -> std::optional<QuestionByKey> {
  564. if (query.isEmpty() || query.size() > _maxKeyLength) {
  565. return {};
  566. }
  567. query = NormalizeKey(query);
  568. for (const auto &[path, file] : _data.files) {
  569. for (const auto &[normalized, question] : file.questions) {
  570. for (const auto &key : question.normalizedKeys) {
  571. if (key == query) {
  572. return QuestionByKey{ question, key };
  573. }
  574. }
  575. }
  576. }
  577. return {};
  578. }
  579. auto Templates::matchFromEnd(QString query) const
  580. -> std::optional<QuestionByKey> {
  581. if (query.size() > _maxKeyLength) {
  582. query = query.mid(query.size() - _maxKeyLength);
  583. }
  584. const auto size = query.size();
  585. auto queries = std::vector<QString>();
  586. queries.reserve(size);
  587. for (auto i = 0; i != size; ++i) {
  588. queries.push_back(NormalizeKey(query.mid(size - i - 1)));
  589. }
  590. auto result = std::optional<QuestionByKey>();
  591. for (const auto &[path, file] : _data.files) {
  592. for (const auto &[normalized, question] : file.questions) {
  593. for (const auto &key : question.normalizedKeys) {
  594. if (key.size() <= queries.size()
  595. && queries[key.size() - 1] == key
  596. && (!result || result->key.size() <= key.size())) {
  597. result = QuestionByKey{ question, key };
  598. }
  599. }
  600. }
  601. }
  602. return result;
  603. }
  604. Templates::~Templates() = default;
  605. auto Templates::query(const QString &text) const -> std::vector<Question> {
  606. const auto words = TextUtilities::PrepareSearchWords(text);
  607. const auto questions = [&](const QString &word) {
  608. const auto i = _index.first.find(word[0]);
  609. return (i == end(_index.first)) ? 0 : i->second.size();
  610. };
  611. const auto best = ranges::min_element(words, std::less<>(), questions);
  612. if (best == std::end(words)) {
  613. return {};
  614. }
  615. const auto narrowed = _index.first.find((*best)[0]);
  616. if (narrowed == end(_index.first)) {
  617. return {};
  618. }
  619. using Id = TemplatesIndex::Id;
  620. using Term = TemplatesIndex::Term;
  621. const auto questionById = [&](const Id &id) {
  622. return _data.files.at(id.first).questions.at(id.second);
  623. };
  624. const auto computeWeight = [&](const Id &id) {
  625. auto result = 0;
  626. const auto full = _index.full.find(id);
  627. for (const auto &word : words) {
  628. const auto from = ranges::lower_bound(
  629. full->second,
  630. word,
  631. std::less<>(),
  632. [](const Term &term) { return term.first; });
  633. const auto till = std::find_if(
  634. from,
  635. end(full->second),
  636. [&](const Term &term) {
  637. return !term.first.startsWith(word);
  638. });
  639. const auto weight = std::max_element(
  640. from,
  641. till,
  642. [](const Term &a, const Term &b) {
  643. return a.second < b.second;
  644. });
  645. if (weight == till) {
  646. return 0;
  647. }
  648. result += weight->second * (weight->first == word ? 2 : 1);
  649. }
  650. return result;
  651. };
  652. using Pair = std::pair<Id, int>;
  653. const auto pairById = [&](const Id &id) {
  654. return std::make_pair(id, computeWeight(id));
  655. };
  656. const auto sorter = [](const Pair &a, const Pair &b) {
  657. // weight DESC filename DESC question ASC
  658. if (a.second > b.second) {
  659. return true;
  660. } else if (a.second < b.second) {
  661. return false;
  662. } else if (a.first.first > b.first.first) {
  663. return true;
  664. } else if (a.first.first < b.first.first) {
  665. return false;
  666. } else {
  667. return (a.first.second < b.first.second);
  668. }
  669. };
  670. const auto good = narrowed->second | ranges::views::transform(
  671. pairById
  672. ) | ranges::views::filter([](const Pair &pair) {
  673. return pair.second > 0;
  674. }) | ranges::to_vector | ranges::actions::stable_sort(sorter);
  675. return good | ranges::views::transform([&](const Pair &pair) {
  676. return questionById(pair.first);
  677. }) | ranges::views::take(kQueryLimit) | ranges::to_vector;
  678. }
  679. } // namespace Support