2018-10-02 20:39:54 +00:00
|
|
|
/*
|
|
|
|
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"
|
|
|
|
|
2018-11-16 05:51:47 +00:00
|
|
|
#include "ui/toast/toast.h"
|
2018-10-05 08:14:00 +00:00
|
|
|
#include "data/data_session.h"
|
2018-11-16 13:36:42 +00:00
|
|
|
#include "core/shortcuts.h"
|
2019-07-24 11:45:24 +00:00
|
|
|
#include "main/main_session.h"
|
2018-10-05 08:14:00 +00:00
|
|
|
|
2019-09-04 07:19:15 +00:00
|
|
|
#include <QtNetwork/QNetworkAccessManager>
|
|
|
|
|
2018-10-02 20:39:54 +00:00
|
|
|
namespace Support {
|
|
|
|
namespace details {
|
|
|
|
namespace {
|
|
|
|
|
|
|
|
constexpr auto kQueryLimit = 10;
|
|
|
|
constexpr auto kWeightStep = 1000;
|
|
|
|
|
2018-10-05 08:14:00 +00:00
|
|
|
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();
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2018-10-02 20:39:54 +00:00
|
|
|
bool IsTemplatesFile(const QString &file) {
|
2022-11-26 21:20:17 +00:00
|
|
|
return file.startsWith(u"tl_"_q, Qt::CaseInsensitive)
|
|
|
|
&& file.endsWith(u".txt"_q, Qt::CaseInsensitive);
|
2018-10-02 20:39:54 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
QString NormalizeQuestion(const QString &question) {
|
|
|
|
auto result = QString();
|
|
|
|
result.reserve(question.size());
|
2021-09-08 10:53:54 +00:00
|
|
|
for (const auto &ch : question) {
|
2018-10-02 20:39:54 +00:00
|
|
|
if (ch.isLetterOrNumber()) {
|
|
|
|
result.append(ch.toLower());
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
2018-11-12 08:52:44 +00:00
|
|
|
QString NormalizeKey(const QString &query) {
|
|
|
|
return TextUtilities::RemoveAccents(query.trimmed().toLower());
|
|
|
|
}
|
|
|
|
|
2018-10-02 20:39:54 +00:00
|
|
|
struct FileResult {
|
|
|
|
TemplatesFile result;
|
|
|
|
QStringList errors;
|
|
|
|
};
|
|
|
|
|
2018-10-05 08:14:00 +00:00
|
|
|
enum class ReadState {
|
|
|
|
None,
|
|
|
|
Question,
|
|
|
|
Keys,
|
|
|
|
Value,
|
|
|
|
Url,
|
|
|
|
};
|
|
|
|
|
|
|
|
template <typename StateChange, typename LineCallback>
|
|
|
|
void ReadByLine(
|
2018-10-11 11:17:51 +00:00
|
|
|
const QByteArray &blob,
|
|
|
|
StateChange &&stateChange,
|
|
|
|
LineCallback &&lineCallback) {
|
2018-10-05 08:14:00 +00:00
|
|
|
using State = ReadState;
|
2018-10-02 20:39:54 +00:00
|
|
|
auto state = State::None;
|
2018-10-05 08:14:00 +00:00
|
|
|
auto hadKeys = false;
|
|
|
|
auto hadValue = false;
|
|
|
|
for (const auto &utf : blob.split('\n')) {
|
2018-10-02 20:39:54 +00:00
|
|
|
const auto line = QString::fromUtf8(utf).trimmed();
|
|
|
|
const auto match = QRegularExpression(
|
2022-11-29 21:46:36 +00:00
|
|
|
u"^\\{([A-Z_]+)\\}$"_q
|
2018-10-02 20:39:54 +00:00
|
|
|
).match(line);
|
|
|
|
if (match.hasMatch()) {
|
|
|
|
const auto token = match.captured(1);
|
2018-10-05 08:14:00 +00:00
|
|
|
if (state == State::Value) {
|
|
|
|
hadKeys = hadValue = false;
|
2018-10-02 20:39:54 +00:00
|
|
|
}
|
2018-10-05 08:14:00 +00:00
|
|
|
const auto newState = [&] {
|
2022-11-26 21:20:17 +00:00
|
|
|
if (token == u"VALUE"_q) {
|
2018-10-05 08:14:00 +00:00
|
|
|
return hadValue ? State::None : State::Value;
|
2022-11-26 21:20:17 +00:00
|
|
|
} else if (token == u"KEYS"_q) {
|
2018-10-05 08:14:00 +00:00
|
|
|
return hadKeys ? State::None : State::Keys;
|
2022-11-26 21:20:17 +00:00
|
|
|
} else if (token == u"QUESTION"_q) {
|
2018-10-05 08:14:00 +00:00
|
|
|
return State::Question;
|
2022-11-26 21:20:17 +00:00
|
|
|
} else if (token == u"URL"_q) {
|
2018-10-05 08:14:00 +00:00
|
|
|
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;
|
|
|
|
}
|
2018-10-02 20:39:54 +00:00
|
|
|
}
|
2018-10-05 08:14:00 +00:00
|
|
|
lineCallback(state, line, false);
|
2018-10-02 20:39:54 +00:00
|
|
|
}
|
2018-10-05 08:14:00 +00:00
|
|
|
}
|
|
|
|
}
|
2018-10-02 20:39:54 +00:00
|
|
|
|
2018-10-05 08:14:00 +00:00
|
|
|
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;
|
|
|
|
}
|
2018-10-02 20:39:54 +00:00
|
|
|
switch (state) {
|
|
|
|
case State::Keys:
|
|
|
|
if (!line.isEmpty()) {
|
2018-11-12 08:52:44 +00:00
|
|
|
question.originalKeys.push_back(line);
|
|
|
|
if (const auto norm = NormalizeKey(line); !norm.isEmpty()) {
|
|
|
|
question.normalizedKeys.push_back(norm);
|
|
|
|
}
|
2018-10-02 20:39:54 +00:00
|
|
|
}
|
|
|
|
break;
|
|
|
|
case State::Value:
|
2018-10-05 08:14:00 +00:00
|
|
|
if (!question.value.isEmpty()) {
|
|
|
|
question.value += '\n';
|
|
|
|
}
|
|
|
|
question.value += line;
|
2018-10-02 20:39:54 +00:00
|
|
|
break;
|
|
|
|
case State::Question:
|
2018-10-05 08:14:00 +00:00
|
|
|
if (question.question.isEmpty()) {
|
|
|
|
question.question = line;
|
|
|
|
}
|
2018-10-02 20:39:54 +00:00
|
|
|
break;
|
|
|
|
case State::Url:
|
2018-10-05 08:14:00 +00:00
|
|
|
if (url.isEmpty()) {
|
|
|
|
url = line;
|
2018-10-02 20:39:54 +00:00
|
|
|
}
|
|
|
|
break;
|
|
|
|
}
|
2018-10-05 08:14:00 +00:00
|
|
|
});
|
|
|
|
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));
|
|
|
|
}
|
|
|
|
});
|
2018-10-02 20:39:54 +00:00
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
|
|
|
FileResult ReadFile(const QString &path) {
|
|
|
|
QFile f(path);
|
|
|
|
if (!f.open(QIODevice::ReadOnly)) {
|
|
|
|
auto result = FileResult();
|
|
|
|
result.errors.push_back(
|
2022-11-29 21:46:36 +00:00
|
|
|
u"Couldn't open '%1' for reading!"_q.arg(path));
|
2018-10-02 20:39:54 +00:00
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
|
|
|
const auto blob = f.readAll();
|
|
|
|
f.close();
|
|
|
|
|
|
|
|
return ReadFromBlob(blob);
|
|
|
|
}
|
|
|
|
|
2018-10-05 08:14:00 +00:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2018-10-02 20:39:54 +00:00
|
|
|
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 = [&](
|
2018-11-12 08:52:44 +00:00
|
|
|
const Id &id,
|
|
|
|
const QString &string,
|
|
|
|
int weight) {
|
2018-10-02 20:39:54 +00:00
|
|
|
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);
|
2018-11-12 08:52:44 +00:00
|
|
|
for (const auto &key : question.normalizedKeys) {
|
2018-10-02 20:39:54 +00:00
|
|
|
pushString(id, key, kWeightStep * kWeightStep);
|
|
|
|
}
|
|
|
|
pushString(id, question.question, kWeightStep);
|
|
|
|
pushString(id, question.value, 1);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
auto result = TemplatesIndex();
|
|
|
|
for (const auto &[ch, unique] : uniqueFirst) {
|
2018-10-05 08:14:00 +00:00
|
|
|
result.first.emplace(ch, unique | ranges::to_vector);
|
2018-10-02 20:39:54 +00:00
|
|
|
}
|
|
|
|
for (const auto &[id, unique] : uniqueFull) {
|
2018-10-05 08:14:00 +00:00
|
|
|
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);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-10-05 16:55:05 +00:00
|
|
|
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)) {
|
2018-11-12 08:52:44 +00:00
|
|
|
question.originalKeys = i->second.originalKeys;
|
|
|
|
question.normalizedKeys = i->second.normalizedKeys;
|
2018-10-05 16:55:05 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-10-05 08:14:00 +00:00
|
|
|
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 {
|
2018-11-12 08:52:44 +00:00
|
|
|
result.keys.emplace(normalized, i->second.originalKeys);
|
2018-10-05 08:14:00 +00:00
|
|
|
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) {
|
2022-11-29 21:46:36 +00:00
|
|
|
auto result = u"Template file '%1' updated!\n\n"_q.arg(path);
|
2018-10-05 08:14:00 +00:00
|
|
|
if (!delta.added.empty()) {
|
2022-11-26 21:20:17 +00:00
|
|
|
result += u"-------- Added --------\n\n"_q;
|
2018-10-05 08:14:00 +00:00
|
|
|
for (const auto question : delta.added) {
|
2022-11-29 21:46:36 +00:00
|
|
|
result += u"Q: %1\nK: %2\nA: %3\n\n"_q.arg(
|
2021-03-13 11:50:34 +00:00
|
|
|
question->question,
|
2022-11-29 21:46:36 +00:00
|
|
|
question->originalKeys.join(u", "_q),
|
2021-03-13 11:50:34 +00:00
|
|
|
question->value.trimmed());
|
2018-10-05 08:14:00 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
if (!delta.changed.empty()) {
|
2022-11-26 21:20:17 +00:00
|
|
|
result += u"-------- Modified --------\n\n"_q;
|
2018-10-05 08:14:00 +00:00
|
|
|
for (const auto question : delta.changed) {
|
2022-11-29 21:46:36 +00:00
|
|
|
result += u"Q: %1\nA: %2\n\n"_q.arg(
|
2021-03-13 19:05:58 +00:00
|
|
|
question->question,
|
|
|
|
question->value.trimmed());
|
2018-10-05 08:14:00 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
if (!delta.removed.empty()) {
|
2022-11-26 21:20:17 +00:00
|
|
|
result += u"-------- Removed --------\n\n"_q;
|
2018-10-05 08:14:00 +00:00
|
|
|
for (const auto question : delta.removed) {
|
2022-11-29 21:46:36 +00:00
|
|
|
result += u"Q: %1\n\n"_q.arg(question->question);
|
2018-10-05 08:14:00 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
|
|
|
QString UpdateFile(
|
2018-10-11 11:17:51 +00:00
|
|
|
const QString &path,
|
|
|
|
const QByteArray &content,
|
|
|
|
const QString &url,
|
|
|
|
const Delta &delta) {
|
2018-10-05 08:14:00 +00:00
|
|
|
auto result = QString();
|
|
|
|
const auto full = cWorkingDir() + "TEMPLATES/" + path;
|
2022-11-26 21:20:17 +00:00
|
|
|
const auto old = full + u".old"_q;
|
2018-10-05 08:14:00 +00:00
|
|
|
QFile(old).remove();
|
|
|
|
if (QFile(full).copy(old)) {
|
2022-11-29 21:46:36 +00:00
|
|
|
result += u"(old file saved at '%1')"_q.arg(path + u".old"_q);
|
2018-10-05 08:14:00 +00:00
|
|
|
|
|
|
|
QFile f(full);
|
|
|
|
if (f.open(QIODevice::WriteOnly)) {
|
|
|
|
WriteWithOwnUrlAndKeys(f, content, url, delta);
|
|
|
|
} else {
|
2022-11-29 21:46:36 +00:00
|
|
|
result += u"\n\nError: could not open new file '%1'!"_q.arg(full);
|
2018-10-05 08:14:00 +00:00
|
|
|
}
|
|
|
|
} else {
|
2022-11-29 21:46:36 +00:00
|
|
|
result += u"Error: could not save old file '%1'!"_q.arg(old);
|
2018-10-02 20:39:54 +00:00
|
|
|
}
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
2018-10-11 11:17:51 +00:00
|
|
|
int CountMaxKeyLength(const TemplatesData &data) {
|
|
|
|
auto result = 0;
|
|
|
|
for (const auto &[path, file] : data.files) {
|
|
|
|
for (const auto &[normalized, question] : file.questions) {
|
2018-11-12 08:52:44 +00:00
|
|
|
for (const auto &key : question.normalizedKeys) {
|
2021-10-19 13:00:21 +00:00
|
|
|
accumulate_max(result, int(key.size()));
|
2018-10-11 11:17:51 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
2018-10-02 20:39:54 +00:00
|
|
|
} // namespace
|
|
|
|
} // namespace details
|
|
|
|
|
2018-10-11 11:17:51 +00:00
|
|
|
using namespace details;
|
|
|
|
|
2018-10-05 08:14:00 +00:00
|
|
|
struct Templates::Updates {
|
|
|
|
QNetworkAccessManager manager;
|
|
|
|
std::map<QString, QNetworkReply*> requests;
|
|
|
|
};
|
|
|
|
|
2019-07-24 11:45:24 +00:00
|
|
|
Templates::Templates(not_null<Main::Session*> session) : _session(session) {
|
2018-11-16 05:51:47 +00:00
|
|
|
load();
|
2018-11-16 13:36:42 +00:00
|
|
|
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);
|
2018-10-02 20:39:54 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
void Templates::reload() {
|
2018-11-16 05:51:47 +00:00
|
|
|
_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() {
|
2018-10-02 20:39:54 +00:00
|
|
|
if (_reloadAfterRead) {
|
|
|
|
return;
|
2019-01-17 08:18:23 +00:00
|
|
|
} else if (_reading || _updates) {
|
2018-10-02 20:39:54 +00:00
|
|
|
_reloadAfterRead = true;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2019-03-27 12:11:38 +00:00
|
|
|
crl::async([=, guard = _reading.make_guard()]() mutable {
|
2018-10-11 11:17:51 +00:00
|
|
|
auto result = ReadFiles(cWorkingDir() + "TEMPLATES");
|
|
|
|
result.index = ComputeIndex(result.result);
|
2019-02-17 11:52:57 +00:00
|
|
|
crl::on_main(std::move(guard), [
|
2018-10-02 20:39:54 +00:00
|
|
|
=,
|
2019-02-17 11:52:57 +00:00
|
|
|
result = std::move(result)
|
2018-10-02 20:39:54 +00:00
|
|
|
]() mutable {
|
2018-11-16 05:51:47 +00:00
|
|
|
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();
|
2018-10-02 20:39:54 +00:00
|
|
|
}
|
|
|
|
});
|
2018-11-16 05:51:47 +00:00
|
|
|
});
|
2018-10-02 20:39:54 +00:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2018-10-11 11:17:51 +00:00
|
|
|
void Templates::setData(TemplatesData &&data) {
|
|
|
|
_data = std::move(data);
|
|
|
|
_maxKeyLength = CountMaxKeyLength(_data);
|
|
|
|
}
|
|
|
|
|
2018-10-05 08:14:00 +00:00
|
|
|
void Templates::ensureUpdatesCreated() {
|
|
|
|
if (_updates) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
_updates = std::make_unique<Updates>();
|
|
|
|
QObject::connect(
|
|
|
|
&_updates->manager,
|
|
|
|
&QNetworkAccessManager::finished,
|
|
|
|
[=](QNetworkReply *reply) { updateRequestFinished(reply); });
|
|
|
|
}
|
|
|
|
|
|
|
|
void Templates::update() {
|
|
|
|
auto errors = QStringList();
|
|
|
|
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) {
|
2022-11-29 21:46:36 +00:00
|
|
|
const auto message = (
|
|
|
|
u"Error: template update failed, url '%1', error %2, %3"_q
|
2018-10-05 08:14:00 +00:00
|
|
|
).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();
|
2018-10-11 11:17:51 +00:00
|
|
|
crl::async([=, weak = base::make_weak(this)]{
|
|
|
|
auto result = ReadFromBlob(content);
|
|
|
|
auto one = TemplatesData();
|
2018-10-05 08:14:00 +00:00
|
|
|
one.files.emplace(path, std::move(result.result));
|
2018-10-11 11:17:51 +00:00
|
|
|
auto index = ComputeIndex(one);
|
|
|
|
crl::on_main(weak,[
|
2018-10-05 08:14:00 +00:00
|
|
|
=,
|
|
|
|
one = std::move(one),
|
|
|
|
errors = std::move(result.errors),
|
|
|
|
index = std::move(index)
|
|
|
|
]() mutable {
|
2018-10-05 16:55:05 +00:00
|
|
|
auto &existing = _data.files.at(path);
|
|
|
|
auto &parsed = one.files.at(path);
|
2018-10-11 11:17:51 +00:00
|
|
|
MoveKeys(parsed, existing);
|
|
|
|
ReplaceFileIndex(_index, ComputeIndex(one), path);
|
2018-10-05 08:14:00 +00:00
|
|
|
if (!errors.isEmpty()) {
|
|
|
|
_errors.fire(std::move(errors));
|
|
|
|
}
|
2018-10-11 11:17:51 +00:00
|
|
|
if (const auto delta = ComputeDelta(existing, parsed)) {
|
|
|
|
const auto text = FormatUpdateNotification(
|
2018-10-05 08:14:00 +00:00
|
|
|
path,
|
|
|
|
delta);
|
2018-10-11 11:17:51 +00:00
|
|
|
const auto copy = UpdateFile(
|
2018-10-05 08:14:00 +00:00
|
|
|
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();
|
|
|
|
});
|
2018-10-11 11:17:51 +00:00
|
|
|
});
|
2018-10-05 08:14:00 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
void Templates::checkUpdateFinished() {
|
|
|
|
if (!_updates || !_updates->requests.empty()) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
_updates = nullptr;
|
|
|
|
if (base::take(_reloadAfterRead)) {
|
|
|
|
reload();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-10-11 11:17:51 +00:00
|
|
|
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) {
|
2018-11-12 08:52:44 +00:00
|
|
|
for (const auto &key : question.normalizedKeys) {
|
2018-10-11 11:17:51 +00:00
|
|
|
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) {
|
2018-11-12 08:52:44 +00:00
|
|
|
for (const auto &key : question.normalizedKeys) {
|
2018-10-11 11:17:51 +00:00
|
|
|
if (key.size() <= queries.size()
|
|
|
|
&& queries[key.size() - 1] == key
|
2018-11-20 12:24:20 +00:00
|
|
|
&& (!result || result->key.size() <= key.size())) {
|
2018-10-11 11:17:51 +00:00
|
|
|
result = QuestionByKey{ question, key };
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
2018-10-05 08:14:00 +00:00
|
|
|
Templates::~Templates() = default;
|
|
|
|
|
2018-10-02 20:39:54 +00:00
|
|
|
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 {};
|
|
|
|
}
|
2018-10-11 11:17:51 +00:00
|
|
|
using Id = TemplatesIndex::Id;
|
|
|
|
using Term = TemplatesIndex::Term;
|
2018-10-02 20:39:54 +00:00
|
|
|
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;
|
|
|
|
};
|
2018-11-20 12:24:20 +00:00
|
|
|
using Pair = std::pair<Id, int>;
|
2018-10-02 20:39:54 +00:00
|
|
|
const auto pairById = [&](const Id &id) {
|
2018-11-20 12:24:20 +00:00
|
|
|
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);
|
|
|
|
}
|
2018-10-02 20:39:54 +00:00
|
|
|
};
|
2021-03-13 12:12:08 +00:00
|
|
|
const auto good = narrowed->second | ranges::views::transform(
|
2018-10-02 20:39:54 +00:00
|
|
|
pairById
|
2021-03-13 12:12:08 +00:00
|
|
|
) | ranges::views::filter([](const Pair &pair) {
|
2018-10-02 20:39:54 +00:00
|
|
|
return pair.second > 0;
|
2021-03-13 18:07:29 +00:00
|
|
|
}) | ranges::to_vector | ranges::actions::stable_sort(sorter);
|
2021-03-13 12:12:08 +00:00
|
|
|
return good | ranges::views::transform([&](const Pair &pair) {
|
2018-11-20 12:24:20 +00:00
|
|
|
return questionById(pair.first);
|
2021-03-13 12:12:08 +00:00
|
|
|
}) | ranges::views::take(kQueryLimit) | ranges::to_vector;
|
2018-10-02 20:39:54 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
} // namespace Support
|