Add autoupdating for templates (support).

This commit is contained in:
John Preston 2018-10-05 11:14:00 +03:00
parent ccaec28d0b
commit 1411dfb711
8 changed files with 425 additions and 70 deletions

View File

@ -1589,11 +1589,6 @@ namespace App {
}
}
bool isValidPhone(QString phone) {
phone = phone.replace(QRegularExpression(qsl("[^\\d]")), QString());
return phone.length() >= 8 || phone == qsl("777") || phone == qsl("333") || phone == qsl("111") || (phone.startsWith(qsl("42")) && (phone.length() == 2 || phone.length() == 5 || phone == qsl("4242")));
}
void quit() {
if (quitting()) {
return;

View File

@ -197,8 +197,6 @@ namespace App {
void checkImageCacheSize();
bool isValidPhone(QString phone);
enum LaunchState {
Launched = 0,
QuitRequested = 1,

View File

@ -36,6 +36,16 @@ constexpr auto kMaxGroupChannelTitle = 255; // See also edit_peer_info_box.
constexpr auto kMaxChannelDescription = 255; // See also edit_peer_info_box.
constexpr auto kMinUsernameLength = 5;
bool IsValidPhone(QString phone) {
phone = phone.replace(QRegularExpression(qsl("[^\\d]")), QString());
return (phone.length() >= 8)
|| (phone == qsl("333"))
|| (phone.startsWith(qsl("42"))
&& (phone.length() == 2
|| phone.length() == 5
|| phone == qsl("4242")));
}
} // namespace
style::InputField CreateBioFieldStyle() {
@ -208,7 +218,7 @@ void AddContactBox::save() {
_first->showError();
}
return;
} else if (!_user && !App::isValidPhone(phone)) {
} else if (!_user && !IsValidPhone(phone)) {
_phone->setFocus();
_phone->showError();
return;

View File

@ -412,7 +412,7 @@ public:
void serviceNotification(
const TextWithEntities &message,
const MTPMessageMedia &media);
const MTPMessageMedia &media = MTP_messageMediaEmpty());
void forgetMedia();

View File

@ -49,6 +49,13 @@ Locale: ") + Platform::SystemLanguage();
UrlClickHandler::Open(url);
}
bool AllowPhoneAttempt(const QString &phone) {
const auto digits = ranges::count_if(
phone,
[](QChar ch) { return ch.isNumber(); });
return (digits > 1);
}
} // namespace
PhoneWidget::PhoneWidget(QWidget *parent, Widget::Data *data) : Step(parent, data)
@ -138,7 +145,8 @@ void PhoneWidget::onInputChange() {
void PhoneWidget::submit() {
if (_sentRequest || isHidden()) return;
if (!App::isValidPhone(fullNumber())) {
const auto phone = fullNumber();
if (!AllowPhoneAttempt(phone)) {
showPhoneError(langFactory(lng_bad_phone));
_phone->setFocus();
return;
@ -148,7 +156,7 @@ void PhoneWidget::submit() {
_checkRequest->start(1000);
_sentPhone = fullNumber();
_sentPhone = phone;
Messenger::Instance().mtp()->setUserPhone(_sentPhone);
//_sentRequest = MTP::send(MTPauth_CheckPhone(MTP_string(_sentPhone)), rpcDone(&PhoneWidget::phoneCheckDone), rpcFail(&PhoneWidget::phoneSubmitFail));
_sentRequest = MTP::send(

View File

@ -27,6 +27,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "core/file_utilities.h"
#include "data/data_session.h"
#include "support/support_common.h"
#include "support/support_templates.h"
#include "auth_session.h"
#include "mainwidget.h"
#include "styles/style_settings.h"
@ -960,8 +961,16 @@ void SetupSupport(not_null<Ui::VerticalLayout*> container) {
Local::writeUserSettings();
}, inner->lifetime());
AddSkip(inner, st::settingsCheckboxesSkip);
AddButton(
inner,
rpl::single(qsl("Reload templates")),
st::settingsButton
)->addClickHandler([=] {
Auth().supportTemplates()->reload();
});
AddSkip(inner);
}
Chat::Chat(QWidget *parent, not_null<UserData*> self)

View File

@ -7,6 +7,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "support/support_templates.h"
#include "data/data_session.h"
#include "auth_session.h"
namespace Support {
namespace details {
namespace {
@ -14,6 +17,18 @@ 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(qstr("tl_"), Qt::CaseInsensitive)
&& file.endsWith(qstr(".txt"), Qt::CaseInsensitive);
@ -35,79 +50,117 @@ struct FileResult {
QStringList errors;
};
FileResult ReadFromBlob(const QByteArray &blob) {
auto result = FileResult();
const auto lines = blob.split('\n');
enum class ReadState {
None,
Question,
Keys,
Value,
Url,
};
enum class State {
None,
Question,
Keys,
Value,
MoreValue,
Url,
};
template <typename StateChange, typename LineCallback>
void ReadByLine(
const QByteArray &blob,
StateChange &&stateChange,
LineCallback &&lineCallback) {
using State = ReadState;
auto state = State::None;
QStringList keys;
QString question, value;
const auto pushQuestion = [&] {
const auto normalized = NormalizeQuestion(question);
if (!normalized.isEmpty()) {
result.result.questions.emplace(
normalized,
TemplatesQuestion{ question, keys, value });
}
question = value = QString();
keys = QStringList();
};
for (const auto &utf : lines) {
auto hadKeys = false;
auto hadValue = false;
for (const auto &utf : blob.split('\n')) {
const auto line = QString::fromUtf8(utf).trimmed();
const auto match = QRegularExpression(
qsl("^\\{([A-Z_]+)\\}$")
).match(line);
if (match.hasMatch()) {
const auto token = match.captured(1);
if (state == State::Value || state == State::MoreValue) {
pushQuestion();
if (state == State::Value) {
hadKeys = hadValue = false;
}
if (token == qstr("VALUE")) {
state = value.isEmpty() ? State::Value : State::None;
} else if (token == qstr("KEYS")) {
state = keys.isEmpty() ? State::Keys : State::None;
} else if (token == qstr("QUESTION")) {
state = State::Question;
} else if (token == qstr("URL")) {
state = State::Url;
} else {
state = State::None;
const auto newState = [&] {
if (token == qstr("VALUE")) {
return hadValue ? State::None : State::Value;
} else if (token == qstr("KEYS")) {
return hadKeys ? State::None : State::Keys;
} else if (token == qstr("QUESTION")) {
return State::Question;
} else if (token == qstr("URL")) {
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;
}
}
continue;
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()) {
keys.push_back(line);
question.keys.push_back(line);
}
break;
case State::MoreValue:
value += '\n';
[[fallthrough]];
case State::Value:
value += line;
state = State::MoreValue;
if (!question.value.isEmpty()) {
question.value += '\n';
}
question.value += line;
break;
case State::Question:
if (question.isEmpty()) question = line;
if (question.question.isEmpty()) {
question.question = line;
}
break;
case State::Url:
if (result.result.url.isEmpty()) {
result.result.url = line;
if (url.isEmpty()) {
url = line;
}
break;
}
}
pushQuestion();
});
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;
}
@ -126,6 +179,64 @@ FileResult ReadFile(const QString &path) {
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;
@ -175,15 +286,123 @@ TemplatesIndex ComputeIndex(const TemplatesData &data) {
}
}
const auto to_vector = [](auto &&range) {
return range | ranges::to_vector;
};
auto result = TemplatesIndex();
for (const auto &[ch, unique] : uniqueFirst) {
result.first.emplace(ch, to_vector(unique));
result.first.emplace(ch, unique | ranges::to_vector);
}
for (const auto &[id, unique] : uniqueFull) {
result.full.emplace(id, to_vector(unique));
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);
}
}
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.keys);
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 = qsl("Template file '%1' updated!\n\n").arg(path);
if (!delta.added.empty()) {
result += qstr("-------- Added --------\n\n");
for (const auto question : delta.added) {
result += qsl("Q: %1\nK: %2\nA: %3\n\n"
).arg(question->question
).arg(question->keys.join(qsl(", "))
).arg(question->value.trimmed());
}
}
if (!delta.changed.empty()) {
result += qstr("-------- Modified --------\n\n");
for (const auto question : delta.changed) {
result += qsl("Q: %1\nA: %2\n\n"
).arg(question->question
).arg(question->value.trimmed());
}
}
if (!delta.removed.empty()) {
result += qstr("-------- Removed --------\n\n");
for (const auto question : delta.removed) {
result += qsl("Q: %1\n\n").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 + qstr(".old");
QFile(old).remove();
if (QFile(full).copy(old)) {
result += qsl("(old file saved at '%1')"
).arg(path + qstr(".old"));
QFile f(full);
if (f.open(QIODevice::WriteOnly)) {
WriteWithOwnUrlAndKeys(f, content, url, delta);
} else {
result += qsl("\n\nError: could not open new file '%1'!"
).arg(full);
}
} else {
result += qsl("Error: could not save old file '%1'!"
).arg(old);
}
return result;
}
@ -191,6 +410,11 @@ TemplatesIndex ComputeIndex(const TemplatesData &data) {
} // namespace
} // namespace details
struct Templates::Updates {
QNetworkAccessManager manager;
std::map<QString, QNetworkReply*> requests;
};
Templates::Templates(not_null<AuthSession*> session) : _session(session) {
reload();
}
@ -198,7 +422,7 @@ Templates::Templates(not_null<AuthSession*> session) : _session(session) {
void Templates::reload() {
if (_reloadAfterRead) {
return;
} else if (_reading.alive()) {
} else if (_reading.alive() || _updates) {
_reloadAfterRead = true;
return;
}
@ -230,6 +454,112 @@ void Templates::reload() {
});
}
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) {
const auto message = qsl(
"Error: template update failed, url '%1', error %2, %3"
).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 = details::ReadFromBlob(content);
auto one = details::TemplatesData();
one.files.emplace(path, std::move(result.result));
auto index = details::ComputeIndex(one);
crl::on_main(weak, [
=,
one = std::move(one),
errors = std::move(result.errors),
index = std::move(index)
]() mutable {
details::ReplaceFileIndex(_index, details::ComputeIndex(one), path);
if (!errors.isEmpty()) {
_errors.fire(std::move(errors));
}
auto &existing = _data.files.at(path);
auto &parsed = one.files.at(path);
if (const auto delta = details::ComputeDelta(existing, parsed)) {
const auto text = details::FormatUpdateNotification(
path,
delta);
const auto copy = details::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();
}
}
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) {
@ -295,8 +625,4 @@ auto Templates::query(const QString &text) const -> std::vector<Question> {
}) | ranges::view::take(details::kQueryLimit) | ranges::to_vector;
}
void Templates::update() {
}
} // namespace Support

View File

@ -52,8 +52,15 @@ public:
return _errors.events();
}
~Templates();
private:
struct Updates;
void update();
void ensureUpdatesCreated();
void updateRequestFinished(QNetworkReply *reply);
void checkUpdateFinished();
not_null<AuthSession*> _session;
@ -63,6 +70,8 @@ private:
base::binary_guard _reading;
bool _reloadAfterRead = false;
std::unique_ptr<Updates> _updates;
};
} // namespace Support