| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739 |
- /*
- 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_templates.h"
- #include "ui/toast/toast.h"
- #include "data/data_session.h"
- #include "core/shortcuts.h"
- #include "main/main_session.h"
- #include <QtNetwork/QNetworkAccessManager>
- namespace Support {
- namespace details {
- namespace {
- constexpr auto kQueryLimit = 10;
- constexpr auto kWeightStep = 1000;
- struct Delta {
- std::vector<const TemplatesQuestion*> added;
- std::vector<const TemplatesQuestion*> changed;
- std::vector<const TemplatesQuestion*> removed;
- std::map<QString, QStringList> keys;
- explicit operator bool() const {
- return !added.empty() || !changed.empty() || !removed.empty();
- }
- };
- bool IsTemplatesFile(const QString &file) {
- return file.startsWith(u"tl_"_q, Qt::CaseInsensitive)
- && file.endsWith(u".txt"_q, Qt::CaseInsensitive);
- }
- QString NormalizeQuestion(const QString &question) {
- auto result = QString();
- result.reserve(question.size());
- for (const auto &ch : question) {
- if (ch.isLetterOrNumber()) {
- result.append(ch.toLower());
- }
- }
- return result;
- }
- QString NormalizeKey(const QString &query) {
- return TextUtilities::RemoveAccents(query.trimmed().toLower());
- }
- struct FileResult {
- TemplatesFile result;
- QStringList errors;
- };
- enum class ReadState {
- None,
- Question,
- Keys,
- Value,
- Url,
- };
- template <typename StateChange, typename LineCallback>
- void ReadByLine(
- const QByteArray &blob,
- StateChange &&stateChange,
- LineCallback &&lineCallback) {
- using State = ReadState;
- auto state = State::None;
- auto hadKeys = false;
- auto hadValue = false;
- for (const auto &utf : blob.split('\n')) {
- const auto line = QString::fromUtf8(utf).trimmed();
- const auto match = QRegularExpression(
- u"^\\{([A-Z_]+)\\}$"_q
- ).match(line);
- if (match.hasMatch()) {
- const auto token = match.captured(1);
- if (state == State::Value) {
- hadKeys = hadValue = false;
- }
- const auto newState = [&] {
- if (token == u"VALUE"_q) {
- return hadValue ? State::None : State::Value;
- } else if (token == u"KEYS"_q) {
- return hadKeys ? State::None : State::Keys;
- } else if (token == u"QUESTION"_q) {
- return State::Question;
- } else if (token == u"URL"_q) {
- return State::Url;
- } else {
- return State::None;
- }
- }();
- stateChange(state, newState);
- state = newState;
- lineCallback(state, line, true);
- } else {
- if (!line.isEmpty()) {
- if (state == State::Value) {
- hadValue = true;
- } else if (state == State::Keys) {
- hadKeys = true;
- }
- }
- lineCallback(state, line, false);
- }
- }
- }
- template <typename Callback>
- QString ReadByLineGetUrl(const QByteArray &blob, Callback &&callback) {
- using State = ReadState;
- auto url = QString();
- auto question = TemplatesQuestion();
- const auto call = [&] {
- while (question.value.endsWith('\n')) {
- question.value.chop(1);
- }
- return callback(base::take(question));
- };
- ReadByLine(blob, [&](State was, State now) {
- if (was == State::Value) {
- call();
- }
- }, [&](State state, const QString &line, bool stateChangeLine) {
- if (stateChangeLine) {
- return;
- }
- switch (state) {
- case State::Keys:
- if (!line.isEmpty()) {
- question.originalKeys.push_back(line);
- if (const auto norm = NormalizeKey(line); !norm.isEmpty()) {
- question.normalizedKeys.push_back(norm);
- }
- }
- break;
- case State::Value:
- if (!question.value.isEmpty()) {
- question.value += '\n';
- }
- question.value += line;
- break;
- case State::Question:
- if (question.question.isEmpty()) {
- question.question = line;
- }
- break;
- case State::Url:
- if (url.isEmpty()) {
- url = line;
- }
- break;
- }
- });
- call();
- return url;
- }
- FileResult ReadFromBlob(const QByteArray &blob) {
- auto result = FileResult();
- result.result.url = ReadByLineGetUrl(blob, [&](TemplatesQuestion &&q) {
- const auto normalized = NormalizeQuestion(q.question);
- if (!normalized.isEmpty()) {
- result.result.questions.emplace(normalized, std::move(q));
- }
- });
- return result;
- }
- FileResult ReadFile(const QString &path) {
- QFile f(path);
- if (!f.open(QIODevice::ReadOnly)) {
- auto result = FileResult();
- result.errors.push_back(
- u"Couldn't open '%1' for reading!"_q.arg(path));
- return result;
- }
- const auto blob = f.readAll();
- f.close();
- return ReadFromBlob(blob);
- }
- void WriteWithOwnUrlAndKeys(
- QIODevice &device,
- const QByteArray &blob,
- const QString &url,
- const Delta &delta) {
- device.write("{URL}\n");
- device.write(url.toUtf8());
- device.write("\n\n");
- using State = ReadState;
- auto question = QString();
- auto normalized = QString();
- auto ownKeysWritten = false;
- ReadByLine(blob, [&](State was, State now) {
- if (was == State::Value) {
- question = normalized = QString();
- }
- }, [&](State state, const QString &line, bool stateChangeLine) {
- const auto writeLine = [&] {
- device.write(line.toUtf8());
- device.write("\n", 1);
- };
- switch (state) {
- case State::Keys:
- if (stateChangeLine) {
- writeLine();
- ownKeysWritten = [&] {
- if (normalized.isEmpty()) {
- return false;
- }
- const auto i = delta.keys.find(normalized);
- if (i == end(delta.keys)) {
- return false;
- }
- device.write(i->second.join('\n').toUtf8());
- device.write("\n", 1);
- return true;
- }();
- } else if (!ownKeysWritten) {
- writeLine();
- }
- break;
- case State::Value:
- writeLine();
- break;
- case State::Question:
- writeLine();
- if (!stateChangeLine && question.isEmpty()) {
- question = line;
- normalized = NormalizeQuestion(line);
- }
- break;
- case State::Url:
- break;
- }
- });
- }
- struct FilesResult {
- TemplatesData result;
- TemplatesIndex index;
- QStringList errors;
- };
- FilesResult ReadFiles(const QString &folder) {
- auto result = FilesResult();
- const auto files = QDir(folder).entryList(QDir::Files);
- for (const auto &path : files) {
- if (!IsTemplatesFile(path)) {
- continue;
- }
- auto file = ReadFile(folder + '/' + path);
- if (!file.result.url.isEmpty() || !file.result.questions.empty()) {
- result.result.files[path] = std::move(file.result);
- }
- result.errors.append(std::move(file.errors));
- }
- return result;
- }
- TemplatesIndex ComputeIndex(const TemplatesData &data) {
- using Id = TemplatesIndex::Id;
- using Term = TemplatesIndex::Term;
- auto uniqueFirst = std::map<QChar, base::flat_set<Id>>();
- auto uniqueFull = std::map<Id, base::flat_set<Term>>();
- const auto pushString = [&](
- const Id &id,
- const QString &string,
- int weight) {
- const auto list = TextUtilities::PrepareSearchWords(string);
- for (const auto &word : list) {
- uniqueFirst[word[0]].emplace(id);
- uniqueFull[id].emplace(std::make_pair(word, weight));
- }
- };
- for (const auto &[path, file] : data.files) {
- for (const auto &[normalized, question] : file.questions) {
- const auto id = std::make_pair(path, normalized);
- for (const auto &key : question.normalizedKeys) {
- pushString(id, key, kWeightStep * kWeightStep);
- }
- pushString(id, question.question, kWeightStep);
- pushString(id, question.value, 1);
- }
- }
- auto result = TemplatesIndex();
- for (const auto &[ch, unique] : uniqueFirst) {
- result.first.emplace(ch, unique | ranges::to_vector);
- }
- for (const auto &[id, unique] : uniqueFull) {
- result.full.emplace(id, unique | ranges::to_vector);
- }
- return result;
- }
- void ReplaceFileIndex(
- TemplatesIndex &result,
- TemplatesIndex &&source,
- const QString &path) {
- for (auto i = begin(result.full); i != end(result.full);) {
- if (i->first.first == path) {
- i = result.full.erase(i);
- } else {
- ++i;
- }
- }
- for (auto &[id, list] : source.full) {
- result.full.emplace(id, std::move(list));
- }
- using Id = TemplatesIndex::Id;
- for (auto &[ch, list] : result.first) {
- auto i = ranges::lower_bound(
- list,
- std::make_pair(path, QString()));
- auto j = std::find_if(i, end(list), [&](const Id &id) {
- return id.first != path;
- });
- list.erase(i, j);
- }
- for (auto &[ch, list] : source.first) {
- auto &to = result.first[ch];
- to.insert(
- end(to),
- std::make_move_iterator(begin(list)),
- std::make_move_iterator(end(list)));
- ranges::sort(to);
- }
- }
- void MoveKeys(TemplatesFile &to, const TemplatesFile &from) {
- const auto &existing = from.questions;
- for (auto &[normalized, question] : to.questions) {
- if (const auto i = existing.find(normalized); i != end(existing)) {
- question.originalKeys = i->second.originalKeys;
- question.normalizedKeys = i->second.normalizedKeys;
- }
- }
- }
- Delta ComputeDelta(const TemplatesFile &was, const TemplatesFile &now) {
- auto result = Delta();
- for (const auto &[normalized, question] : now.questions) {
- const auto i = was.questions.find(normalized);
- if (i == end(was.questions)) {
- result.added.push_back(&question);
- } else {
- result.keys.emplace(normalized, i->second.originalKeys);
- if (i->second.value != question.value) {
- result.changed.push_back(&question);
- }
- }
- }
- for (const auto &[normalized, question] : was.questions) {
- if (result.keys.find(normalized) == end(result.keys)) {
- result.removed.push_back(&question);
- }
- }
- return result;
- }
- QString FormatUpdateNotification(const QString &path, const Delta &delta) {
- auto result = u"Template file '%1' updated!\n\n"_q.arg(path);
- if (!delta.added.empty()) {
- result += u"-------- Added --------\n\n"_q;
- for (const auto question : delta.added) {
- result += u"Q: %1\nK: %2\nA: %3\n\n"_q.arg(
- question->question,
- question->originalKeys.join(u", "_q),
- question->value.trimmed());
- }
- }
- if (!delta.changed.empty()) {
- result += u"-------- Modified --------\n\n"_q;
- for (const auto question : delta.changed) {
- result += u"Q: %1\nA: %2\n\n"_q.arg(
- question->question,
- question->value.trimmed());
- }
- }
- if (!delta.removed.empty()) {
- result += u"-------- Removed --------\n\n"_q;
- for (const auto question : delta.removed) {
- result += u"Q: %1\n\n"_q.arg(question->question);
- }
- }
- return result;
- }
- QString UpdateFile(
- const QString &path,
- const QByteArray &content,
- const QString &url,
- const Delta &delta) {
- auto result = QString();
- const auto full = cWorkingDir() + "TEMPLATES/" + path;
- const auto old = full + u".old"_q;
- QFile(old).remove();
- if (QFile(full).copy(old)) {
- result += u"(old file saved at '%1')"_q.arg(path + u".old"_q);
- QFile f(full);
- if (f.open(QIODevice::WriteOnly)) {
- WriteWithOwnUrlAndKeys(f, content, url, delta);
- } else {
- result += u"\n\nError: could not open new file '%1'!"_q.arg(full);
- }
- } else {
- result += u"Error: could not save old file '%1'!"_q.arg(old);
- }
- return result;
- }
- int CountMaxKeyLength(const TemplatesData &data) {
- auto result = 0;
- for (const auto &[path, file] : data.files) {
- for (const auto &[normalized, question] : file.questions) {
- for (const auto &key : question.normalizedKeys) {
- accumulate_max(result, int(key.size()));
- }
- }
- }
- return result;
- }
- } // namespace
- } // namespace details
- using namespace details;
- struct Templates::Updates {
- QNetworkAccessManager manager;
- std::map<QString, QNetworkReply*> requests;
- };
- Templates::Templates(not_null<Main::Session*> session) : _session(session) {
- load();
- Shortcuts::Requests(
- ) | rpl::start_with_next([=](not_null<Shortcuts::Request*> request) {
- using Command = Shortcuts::Command;
- request->check(
- Command::SupportReloadTemplates
- ) && request->handle([=] {
- reload();
- return true;
- });
- }, _lifetime);
- }
- void Templates::reload() {
- _reloadToastSubscription = errors(
- ) | rpl::start_with_next([=](QStringList errors) {
- Ui::Toast::Show(errors.isEmpty()
- ? "Templates reloaded!"
- : ("Errors:\n\n" + errors.join("\n\n")));
- });
- load();
- }
- void Templates::load() {
- if (_reloadAfterRead) {
- return;
- } else if (_reading || _updates) {
- _reloadAfterRead = true;
- return;
- }
- crl::async([=, guard = _reading.make_guard()]() mutable {
- auto result = ReadFiles(cWorkingDir() + "TEMPLATES");
- result.index = ComputeIndex(result.result);
- crl::on_main(std::move(guard), [
- =,
- result = std::move(result)
- ]() mutable {
- setData(std::move(result.result));
- _index = std::move(result.index);
- _errors.fire(std::move(result.errors));
- crl::on_main(this, [=] {
- if (base::take(_reloadAfterRead)) {
- reload();
- } else {
- update();
- }
- });
- });
- });
- }
- void Templates::setData(TemplatesData &&data) {
- _data = std::move(data);
- _maxKeyLength = CountMaxKeyLength(_data);
- }
- void Templates::ensureUpdatesCreated() {
- if (_updates) {
- return;
- }
- _updates = std::make_unique<Updates>();
- QObject::connect(
- &_updates->manager,
- &QNetworkAccessManager::finished,
- [=](QNetworkReply *reply) { updateRequestFinished(reply); });
- }
- void Templates::update() {
- const auto sendRequest = [&](const QString &path, const QString &url) {
- ensureUpdatesCreated();
- if (_updates->requests.find(path) != end(_updates->requests)) {
- return;
- }
- _updates->requests.emplace(
- path,
- _updates->manager.get(QNetworkRequest(url)));
- };
- for (const auto &[path, file] : _data.files) {
- if (!file.url.isEmpty()) {
- sendRequest(path, file.url);
- }
- }
- }
- void Templates::updateRequestFinished(QNetworkReply *reply) {
- reply->deleteLater();
- const auto path = [&] {
- for (const auto &[file, sent] : _updates->requests) {
- if (sent == reply) {
- return file;
- }
- }
- return QString();
- }();
- if (path.isEmpty()) {
- return;
- }
- _updates->requests[path] = nullptr;
- if (reply->error() != QNetworkReply::NoError) {
- const auto message = (
- u"Error: template update failed, url '%1', error %2, %3"_q
- ).arg(reply->url().toDisplayString()
- ).arg(reply->error()
- ).arg(reply->errorString());
- _session->data().serviceNotification({ message });
- return;
- }
- LOG(("Got template from url '%1'"
- ).arg(reply->url().toDisplayString()));
- const auto content = reply->readAll();
- crl::async([=, weak = base::make_weak(this)]{
- auto result = ReadFromBlob(content);
- auto one = TemplatesData();
- one.files.emplace(path, std::move(result.result));
- auto index = ComputeIndex(one);
- crl::on_main(weak,[
- =,
- one = std::move(one),
- errors = std::move(result.errors),
- index = std::move(index)
- ]() mutable {
- auto &existing = _data.files.at(path);
- auto &parsed = one.files.at(path);
- MoveKeys(parsed, existing);
- ReplaceFileIndex(_index, ComputeIndex(one), path);
- if (!errors.isEmpty()) {
- _errors.fire(std::move(errors));
- }
- if (const auto delta = ComputeDelta(existing, parsed)) {
- const auto text = FormatUpdateNotification(
- path,
- delta);
- const auto copy = UpdateFile(
- path,
- content,
- existing.url,
- delta);
- const auto full = text + copy;
- _session->data().serviceNotification({ full });
- }
- _data.files.at(path) = std::move(one.files.at(path));
- _updates->requests.erase(path);
- checkUpdateFinished();
- });
- });
- }
- void Templates::checkUpdateFinished() {
- if (!_updates || !_updates->requests.empty()) {
- return;
- }
- _updates = nullptr;
- if (base::take(_reloadAfterRead)) {
- reload();
- }
- }
- auto Templates::matchExact(QString query) const
- -> std::optional<QuestionByKey> {
- if (query.isEmpty() || query.size() > _maxKeyLength) {
- return {};
- }
- query = NormalizeKey(query);
- for (const auto &[path, file] : _data.files) {
- for (const auto &[normalized, question] : file.questions) {
- for (const auto &key : question.normalizedKeys) {
- if (key == query) {
- return QuestionByKey{ question, key };
- }
- }
- }
- }
- return {};
- }
- auto Templates::matchFromEnd(QString query) const
- -> std::optional<QuestionByKey> {
- if (query.size() > _maxKeyLength) {
- query = query.mid(query.size() - _maxKeyLength);
- }
- const auto size = query.size();
- auto queries = std::vector<QString>();
- queries.reserve(size);
- for (auto i = 0; i != size; ++i) {
- queries.push_back(NormalizeKey(query.mid(size - i - 1)));
- }
- auto result = std::optional<QuestionByKey>();
- for (const auto &[path, file] : _data.files) {
- for (const auto &[normalized, question] : file.questions) {
- for (const auto &key : question.normalizedKeys) {
- if (key.size() <= queries.size()
- && queries[key.size() - 1] == key
- && (!result || result->key.size() <= key.size())) {
- result = QuestionByKey{ question, key };
- }
- }
- }
- }
- return result;
- }
- Templates::~Templates() = default;
- auto Templates::query(const QString &text) const -> std::vector<Question> {
- const auto words = TextUtilities::PrepareSearchWords(text);
- const auto questions = [&](const QString &word) {
- const auto i = _index.first.find(word[0]);
- return (i == end(_index.first)) ? 0 : i->second.size();
- };
- const auto best = ranges::min_element(words, std::less<>(), questions);
- if (best == std::end(words)) {
- return {};
- }
- const auto narrowed = _index.first.find((*best)[0]);
- if (narrowed == end(_index.first)) {
- return {};
- }
- using Id = TemplatesIndex::Id;
- using Term = TemplatesIndex::Term;
- const auto questionById = [&](const Id &id) {
- return _data.files.at(id.first).questions.at(id.second);
- };
- const auto computeWeight = [&](const Id &id) {
- auto result = 0;
- const auto full = _index.full.find(id);
- for (const auto &word : words) {
- const auto from = ranges::lower_bound(
- full->second,
- word,
- std::less<>(),
- [](const Term &term) { return term.first; });
- const auto till = std::find_if(
- from,
- end(full->second),
- [&](const Term &term) {
- return !term.first.startsWith(word);
- });
- const auto weight = std::max_element(
- from,
- till,
- [](const Term &a, const Term &b) {
- return a.second < b.second;
- });
- if (weight == till) {
- return 0;
- }
- result += weight->second * (weight->first == word ? 2 : 1);
- }
- return result;
- };
- using Pair = std::pair<Id, int>;
- const auto pairById = [&](const Id &id) {
- return std::make_pair(id, computeWeight(id));
- };
- const auto sorter = [](const Pair &a, const Pair &b) {
- // weight DESC filename DESC question ASC
- if (a.second > b.second) {
- return true;
- } else if (a.second < b.second) {
- return false;
- } else if (a.first.first > b.first.first) {
- return true;
- } else if (a.first.first < b.first.first) {
- return false;
- } else {
- return (a.first.second < b.first.second);
- }
- };
- const auto good = narrowed->second | ranges::views::transform(
- pairById
- ) | ranges::views::filter([](const Pair &pair) {
- return pair.second > 0;
- }) | ranges::to_vector | ranges::actions::stable_sort(sorter);
- return good | ranges::views::transform([&](const Pair &pair) {
- return questionById(pair.first);
- }) | ranges::views::take(kQueryLimit) | ranges::to_vector;
- }
- } // namespace Support
|