From 1411dfb71192184f97bfa4ae527e405989a6986c Mon Sep 17 00:00:00 2001 From: John Preston Date: Fri, 5 Oct 2018 11:14:00 +0300 Subject: [PATCH] Add autoupdating for templates (support). --- Telegram/SourceFiles/app.cpp | 5 - Telegram/SourceFiles/app.h | 2 - .../SourceFiles/boxes/add_contact_box.cpp | 12 +- Telegram/SourceFiles/data/data_session.h | 2 +- Telegram/SourceFiles/intro/introphone.cpp | 12 +- .../SourceFiles/settings/settings_chat.cpp | 11 +- .../SourceFiles/support/support_templates.cpp | 442 +++++++++++++++--- .../SourceFiles/support/support_templates.h | 9 + 8 files changed, 425 insertions(+), 70 deletions(-) diff --git a/Telegram/SourceFiles/app.cpp b/Telegram/SourceFiles/app.cpp index 08f819886b..e794571f36 100644 --- a/Telegram/SourceFiles/app.cpp +++ b/Telegram/SourceFiles/app.cpp @@ -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; diff --git a/Telegram/SourceFiles/app.h b/Telegram/SourceFiles/app.h index d9a5517bf9..acdc7ede5c 100644 --- a/Telegram/SourceFiles/app.h +++ b/Telegram/SourceFiles/app.h @@ -197,8 +197,6 @@ namespace App { void checkImageCacheSize(); - bool isValidPhone(QString phone); - enum LaunchState { Launched = 0, QuitRequested = 1, diff --git a/Telegram/SourceFiles/boxes/add_contact_box.cpp b/Telegram/SourceFiles/boxes/add_contact_box.cpp index 47fb035174..f991b2035f 100644 --- a/Telegram/SourceFiles/boxes/add_contact_box.cpp +++ b/Telegram/SourceFiles/boxes/add_contact_box.cpp @@ -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; diff --git a/Telegram/SourceFiles/data/data_session.h b/Telegram/SourceFiles/data/data_session.h index 81b75122ed..da2099e01c 100644 --- a/Telegram/SourceFiles/data/data_session.h +++ b/Telegram/SourceFiles/data/data_session.h @@ -412,7 +412,7 @@ public: void serviceNotification( const TextWithEntities &message, - const MTPMessageMedia &media); + const MTPMessageMedia &media = MTP_messageMediaEmpty()); void forgetMedia(); diff --git a/Telegram/SourceFiles/intro/introphone.cpp b/Telegram/SourceFiles/intro/introphone.cpp index 9603f9bba8..030fd8770e 100644 --- a/Telegram/SourceFiles/intro/introphone.cpp +++ b/Telegram/SourceFiles/intro/introphone.cpp @@ -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( diff --git a/Telegram/SourceFiles/settings/settings_chat.cpp b/Telegram/SourceFiles/settings/settings_chat.cpp index c1dd430e0b..8c93409e70 100644 --- a/Telegram/SourceFiles/settings/settings_chat.cpp +++ b/Telegram/SourceFiles/settings/settings_chat.cpp @@ -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 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 self) diff --git a/Telegram/SourceFiles/support/support_templates.cpp b/Telegram/SourceFiles/support/support_templates.cpp index 6ecf752660..cabad99a20 100644 --- a/Telegram/SourceFiles/support/support_templates.cpp +++ b/Telegram/SourceFiles/support/support_templates.cpp @@ -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 added; + std::vector changed; + std::vector removed; + + std::map 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 +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 +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 requests; +}; + Templates::Templates(not_null session) : _session(session) { reload(); } @@ -198,7 +422,7 @@ Templates::Templates(not_null 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(); + 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 { 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 { }) | ranges::view::take(details::kQueryLimit) | ranges::to_vector; } -void Templates::update() { - -} - } // namespace Support diff --git a/Telegram/SourceFiles/support/support_templates.h b/Telegram/SourceFiles/support/support_templates.h index 4d5b366b98..56292d910e 100644 --- a/Telegram/SourceFiles/support/support_templates.h +++ b/Telegram/SourceFiles/support/support_templates.h @@ -52,8 +52,15 @@ public: return _errors.events(); } + ~Templates(); + private: + struct Updates; + void update(); + void ensureUpdatesCreated(); + void updateRequestFinished(QNetworkReply *reply); + void checkUpdateFinished(); not_null _session; @@ -63,6 +70,8 @@ private: base::binary_guard _reading; bool _reloadAfterRead = false; + std::unique_ptr _updates; + }; } // namespace Support