From 7b377511a94a51a26ec68a1c3b20e5da55918e6e Mon Sep 17 00:00:00 2001 From: Stefan Talpalaru Date: Sun, 20 Feb 2022 11:54:20 -0600 Subject: [PATCH] feat: default public trackers --- daemon/daemon.cc | 7 +++- gtk/Application.cc | 4 +++ gtk/PrefsDialog.cc | 32 ++++++++++++++++++ libtransmission/quark.cc | 3 +- libtransmission/quark.h | 1 + libtransmission/rpcimpl.cc | 9 ++++++ libtransmission/session.cc | 54 +++++++++++++++++++++++++++++++ libtransmission/session.h | 12 +++++++ libtransmission/torrent.cc | 47 +++++++++++++++++++++++++++ libtransmission/transmission.h | 2 ++ qt/Prefs.cc | 1 + qt/Prefs.h | 1 + qt/PrefsDialog.cc | 33 +++++++++++++++++++ qt/PrefsDialog.h | 1 + qt/PrefsDialog.ui | 20 ++++++++++++ qt/Session.cc | 1 + tests/libtransmission/rpc-test.cc | 3 +- web/src/prefs-dialog.js | 33 +++++++++++++++++++ web/style/transmission-app.scss | 4 +++ 19 files changed, 265 insertions(+), 3 deletions(-) diff --git a/daemon/daemon.cc b/daemon/daemon.cc index a1e80afdc..f9794c5ef 100644 --- a/daemon/daemon.cc +++ b/daemon/daemon.cc @@ -94,7 +94,7 @@ static struct event_base* ev_base = nullptr; **** Config File ***/ -static auto constexpr Options = std::array{ +static auto constexpr Options = std::array{ { { 'a', "allowed", "Allowed IP addresses. (Default: " TR_DEFAULT_RPC_WHITELIST ")", "a", true, "" }, { 'b', "blocklist", "Enable peer blocklists", "b", false, nullptr }, { 'B', "no-blocklist", "Disable peer blocklists", "B", false, nullptr }, @@ -103,6 +103,7 @@ static auto constexpr Options = std::array{ { 941, "incomplete-dir", "Where to store new torrents until they're complete", nullptr, true, "" }, { 942, "no-incomplete-dir", "Don't store incomplete torrents in a different location", nullptr, false, nullptr }, { 'd', "dump-settings", "Dump the settings and exit", "d", false, nullptr }, + { 943, "default-trackers", "Trackers for public torrents to use automatically", nullptr, true, "" }, { 'e', "logfile", "Dump the log messages to this filename", "e", true, "" }, { 'f', "foreground", "Run in the foreground instead of daemonizing", "f", false, nullptr }, { 'g', "config-dir", "Where to look for configuration files", "g", true, "" }, @@ -416,6 +417,10 @@ static bool parse_args( tr_variantDictAddBool(settings, TR_KEY_incomplete_dir_enabled, false); break; + case 943: + tr_variantDictAddStr(settings, TR_KEY_default_trackers, optstr); + break; + case 'd': *dump_settings = true; break; diff --git a/gtk/Application.cc b/gtk/Application.cc index edf5ae82b..f8cc011f6 100644 --- a/gtk/Application.cc +++ b/gtk/Application.cc @@ -1022,6 +1022,10 @@ void Application::Impl::on_prefs_changed(tr_quark const key) tr_sessionSetEncryption(tr, static_cast(gtr_pref_int_get(key))); break; + case TR_KEY_default_trackers: + tr_sessionSetDefaultTrackers(tr, gtr_pref_string_get(key).c_str()); + break; + case TR_KEY_download_dir: tr_sessionSetDownloadDir(tr, gtr_pref_string_get(key).c_str()); break; diff --git a/gtk/PrefsDialog.cc b/gtk/PrefsDialog.cc index e17725097..54f9d3c7a 100644 --- a/gtk/PrefsDialog.cc +++ b/gtk/PrefsDialog.cc @@ -176,6 +176,33 @@ Gtk::Entry* new_entry(tr_quark const key, Glib::RefPtr const& core) return w; } +void text_buffer_changed_cb(Glib::RefPtr buffer, tr_quark const key, Glib::RefPtr const& core) +{ + Gtk::TextBuffer::iterator start, end; + buffer->get_bounds(start, end); + core->set_pref(key, buffer->get_text(start, end, FALSE)); +} + +Gtk::Widget* new_text_view(tr_quark const key, Glib::RefPtr const& core) +{ + auto* w = Gtk::make_managed(); + auto buffer = w->get_buffer(); + + buffer->set_text(gtr_pref_string_get(key)); + + /* set up the scrolled window and put the text view in it */ + auto* scroll = Gtk::make_managed(); + scroll->set_policy(Gtk::PolicyType::POLICY_AUTOMATIC, Gtk::PolicyType::POLICY_AUTOMATIC); + scroll->set_shadow_type(Gtk::ShadowType::SHADOW_IN); + scroll->add(*w); + scroll->set_size_request(-1, 200); + + /* signal */ + buffer->signal_changed().connect([buffer, key, core]() { text_buffer_changed_cb(buffer, key, core); }); + + return scroll; +} + void chosen_cb(Gtk::FileChooser* w, tr_quark const key, Glib::RefPtr const& core) { core->set_pref(key, w->get_filename()); @@ -1051,6 +1078,11 @@ Gtk::Widget* PrefsDialog::Impl::networkPage() w->set_tooltip_text(_("LPD is a tool for finding peers on your local network.")); t->add_wide_control(row, *w); + t->add_section_title(row, _("Default Trackers")); + auto tv = new_text_view(TR_KEY_default_trackers, core_); + tv->set_tooltip_text(_("Trackers for public torrents to use automatically")); + t->add_wide_control(row, *tv); + return t; } diff --git a/libtransmission/quark.cc b/libtransmission/quark.cc index 6a2d1ba93..7e60c886a 100644 --- a/libtransmission/quark.cc +++ b/libtransmission/quark.cc @@ -18,7 +18,7 @@ using namespace std::literals; namespace { -auto constexpr my_static = std::array{ ""sv, +auto constexpr my_static = std::array{ ""sv, "activeTorrentCount"sv, "activity-date"sv, "activityDate"sv, @@ -74,6 +74,7 @@ auto constexpr my_static = std::array{ ""sv, "current-stats"sv, "date"sv, "dateCreated"sv, + "default-trackers"sv, "delete-local-data"sv, "desiredAvailable"sv, "destination"sv, diff --git a/libtransmission/quark.h b/libtransmission/quark.h index 2afd6805e..a9e76a30e 100644 --- a/libtransmission/quark.h +++ b/libtransmission/quark.h @@ -77,6 +77,7 @@ enum TR_KEY_current_stats, TR_KEY_date, TR_KEY_dateCreated, + TR_KEY_default_trackers, TR_KEY_delete_local_data, TR_KEY_desiredAvailable, TR_KEY_destination, diff --git a/libtransmission/rpcimpl.cc b/libtransmission/rpcimpl.cc index 898cfe11b..365fb8440 100644 --- a/libtransmission/rpcimpl.cc +++ b/libtransmission/rpcimpl.cc @@ -1789,6 +1789,11 @@ static char const* sessionSet( tr_sessionSetQueueStalledEnabled(session, boolVal); } + if (tr_variantDictFindStrView(args_in, TR_KEY_default_trackers, &sv)) + { + session->setDefaultTrackers(sv); + } + if (tr_variantDictFindInt(args_in, TR_KEY_download_queue_size, &i)) { tr_sessionSetQueueSize(session, TR_DOWN, (int)i); @@ -2071,6 +2076,10 @@ static void addSessionField(tr_session* s, tr_variant* d, tr_quark key) tr_variantDictAddStr(d, key, tr_sessionGetConfigDir(s)); break; + case TR_KEY_default_trackers: + tr_variantDictAddStr(d, key, s->defaultTrackers()); + break; + case TR_KEY_download_dir: tr_variantDictAddStr(d, key, s->downloadDir()); break; diff --git a/libtransmission/session.cc b/libtransmission/session.cc index 9d26a3a9c..4f21f367b 100644 --- a/libtransmission/session.cc +++ b/libtransmission/session.cc @@ -333,6 +333,7 @@ void tr_sessionGetDefaultSettings(tr_variant* d) tr_variantDictAddBool(d, TR_KEY_utp_enabled, true); tr_variantDictAddBool(d, TR_KEY_lpd_enabled, false); tr_variantDictAddStr(d, TR_KEY_download_dir, tr_getDefaultDownloadDir()); + tr_variantDictAddStr(d, TR_KEY_default_trackers, ""); tr_variantDictAddInt(d, TR_KEY_speed_limit_down, 100); tr_variantDictAddBool(d, TR_KEY_speed_limit_down_enabled, false); tr_variantDictAddInt(d, TR_KEY_encryption, TR_DEFAULT_ENCRYPTION); @@ -411,6 +412,7 @@ void tr_sessionGetSettings(tr_session const* s, tr_variant* d) tr_variantDictAddBool(d, TR_KEY_utp_enabled, s->isUTPEnabled); tr_variantDictAddBool(d, TR_KEY_lpd_enabled, s->isLPDEnabled); tr_variantDictAddStr(d, TR_KEY_download_dir, tr_sessionGetDownloadDir(s)); + tr_variantDictAddStr(d, TR_KEY_default_trackers, s->defaultTrackers()); tr_variantDictAddInt(d, TR_KEY_download_queue_size, tr_sessionGetQueueSize(s, TR_DOWN)); tr_variantDictAddBool(d, TR_KEY_download_queue_enabled, tr_sessionGetQueueEnabled(s, TR_DOWN)); tr_variantDictAddInt(d, TR_KEY_speed_limit_down, tr_sessionGetSpeedLimit_KBps(s, TR_DOWN)); @@ -811,6 +813,11 @@ static void sessionSetImpl(void* vdata) tr_sessionSetCacheLimit_MB(session, i); } + if (tr_variantDictFindStrView(settings, TR_KEY_default_trackers, &sv)) + { + session->setDefaultTrackers(sv); + } + if (tr_variantDictFindInt(settings, TR_KEY_peer_limit_per_torrent, &i)) { tr_sessionSetPeerLimitPerTorrent(session, i); @@ -2244,6 +2251,53 @@ int tr_sessionGetCacheLimit_MB(tr_session const* session) **** ***/ +void tr_session::setDefaultTrackers(std::string_view trackers) +{ + /* keep the string */ + this->default_trackers_str_ = trackers; + + /* clear out the old list entries */ + this->defaultTrackersList.clear(); + + /* build the new list entries */ + auto urlStart = std::string::npos; + auto constexpr Delimiters = " ,;\r\n\t"sv; + auto fragment = default_trackers_str_; + while ((urlStart = fragment.find_first_not_of(Delimiters)) != std::string::npos) + { + auto urlEnd = fragment.find_first_of(Delimiters, urlStart); + if (urlEnd == std::string::npos) + { + urlEnd = fragment.size() - 1; + } + else + { + urlEnd -= 1; + } + this->defaultTrackersList.push_back(fragment.substr(urlStart, urlEnd)); + + if (fragment.size() > (urlEnd + 1)) + { + fragment = fragment.substr(urlEnd + 1, fragment.size()); + } + else + { + break; + } + } +} + +void tr_sessionSetDefaultTrackers(tr_session* session, char const* trackers) +{ + TR_ASSERT(tr_isSession(session)); + + session->setDefaultTrackers(trackers ? trackers : ""); +} + +/*** +**** +***/ + struct port_forwarding_data { bool enabled; diff --git a/libtransmission/session.h b/libtransmission/session.h index a68173ae5..33ed35c2e 100644 --- a/libtransmission/session.h +++ b/libtransmission/session.h @@ -150,6 +150,15 @@ public: download_dir_ = dir; } + // default trackers + + std::string const& defaultTrackers() const + { + return default_trackers_str_; + } + + void setDefaultTrackers(std::string_view trackers); + // incomplete dir std::string const& incompleteDir() const @@ -387,6 +396,8 @@ public: std::unique_ptr rpc_server_; + std::list defaultTrackersList; + // One of 's IPTOS_ values. // See tr_netTos*() in libtransmission/net.h for more info // Only session.cc should use this. @@ -398,6 +409,7 @@ private: std::array scripts_; std::string blocklist_url_; std::string download_dir_; + std::string default_trackers_str_; std::string incomplete_dir_; std::string peer_congestion_algorithm_; diff --git a/libtransmission/torrent.cc b/libtransmission/torrent.cc index 3c29ef7d8..f39be7ce7 100644 --- a/libtransmission/torrent.cc +++ b/libtransmission/torrent.cc @@ -817,6 +817,52 @@ static void torrentInit(tr_torrent* tor, tr_ctor const* ctor) } } +static void tr_torrentAddDefaultTrackers(tr_torrent* tor) +{ + std::list trackerURLs = {}; + int numExistingTrackers = tr_torrentTrackerCount(tor); + int numNewTrackers = tor->session->defaultTrackersList.size(); + + if (!numNewTrackers || tor->isPrivate()) + { + return; + } + + // copy existing tracker URLs + for (int i = 0; i < numExistingTrackers; ++i) + { + auto tracker = tr_torrentTracker(tor, i); + trackerURLs.push_back(tracker.announce); + } + + // add the new ones + for (std::string_view url : tor->session->defaultTrackersList) + { + if (tr_urlIsValidTracker(url)) + { + // check for duplicates + bool duplicate = false; + for (auto trackerURL : trackerURLs) + { + if (trackerURL == url) + { + duplicate = true; + break; + } + } + + if (duplicate) + { + continue; + } + + tor->announceList().add(url); + } + } + /* tell the announcer to reload this torrent's tracker list */ + tr_announcerResetTorrent(tor->session->announcer, tor); +} + tr_torrent* tr_torrentNew(tr_ctor* ctor, tr_torrent** setme_duplicate_of) { TR_ASSERT(ctor != nullptr); @@ -843,6 +889,7 @@ tr_torrent* tr_torrentNew(tr_ctor* ctor, tr_torrent** setme_duplicate_of) auto* const tor = new tr_torrent{ std::move(metainfo) }; torrentInit(tor, ctor); + tr_torrentAddDefaultTrackers(tor); return tor; } diff --git a/libtransmission/transmission.h b/libtransmission/transmission.h index 7b8f6b75c..63383250d 100644 --- a/libtransmission/transmission.h +++ b/libtransmission/transmission.h @@ -392,6 +392,8 @@ bool tr_sessionIsRPCPasswordEnabled(tr_session const* session); char const* tr_sessionGetRPCBindAddress(tr_session const* session); +void tr_sessionSetDefaultTrackers(tr_session* session, char const* trackers); + enum tr_rpc_callback_type { TR_RPC_TORRENT_ADDED, diff --git a/qt/Prefs.cc b/qt/Prefs.cc index 48b8b1dd0..495e327d9 100644 --- a/qt/Prefs.cc +++ b/qt/Prefs.cc @@ -109,6 +109,7 @@ std::array const Prefs::Items{ { ALT_SPEED_LIMIT_TIME_DAY, TR_KEY_alt_speed_time_day, QVariant::Int }, { BLOCKLIST_ENABLED, TR_KEY_blocklist_enabled, QVariant::Bool }, { BLOCKLIST_URL, TR_KEY_blocklist_url, QVariant::String }, + { DEFAULT_TRACKERS, TR_KEY_default_trackers, QVariant::String }, { DSPEED, TR_KEY_speed_limit_down, QVariant::Int }, { DSPEED_ENABLED, TR_KEY_speed_limit_down_enabled, QVariant::Bool }, { DOWNLOAD_DIR, TR_KEY_download_dir, QVariant::String }, diff --git a/qt/Prefs.h b/qt/Prefs.h index dc27c57b0..6bb7715a7 100644 --- a/qt/Prefs.h +++ b/qt/Prefs.h @@ -83,6 +83,7 @@ public: ALT_SPEED_LIMIT_TIME_DAY, BLOCKLIST_ENABLED, BLOCKLIST_URL, + DEFAULT_TRACKERS, DSPEED, DSPEED_ENABLED, DOWNLOAD_DIR, diff --git a/qt/PrefsDialog.cc b/qt/PrefsDialog.cc index 59437c96b..a4488c0bd 100644 --- a/qt/PrefsDialog.cc +++ b/qt/PrefsDialog.cc @@ -190,6 +190,10 @@ bool PrefsDialog::updateWidgetValue(QWidget* widget, int pref_key) const { pref_widget.as()->setPath(prefs_.getString(pref_key)); } + else if (pref_widget.is()) + { + pref_widget.as()->setPlainText(prefs_.getString(pref_key)); + } else { return false; @@ -233,6 +237,14 @@ void PrefsDialog::linkWidgetToPref(QWidget* widget, int pref_key) if (auto const* spin_box = qobject_cast(widget); spin_box != nullptr) { connect(spin_box, &QAbstractSpinBox::editingFinished, this, &PrefsDialog::spinBoxEditingFinished); + return; + } + + auto const* plain_text_edit = qobject_cast(widget); + if (plain_text_edit != nullptr) + { + connect(plain_text_edit, &QPlainTextEdit::textChanged, this, &PrefsDialog::plainTextChanged); + return; } } @@ -295,6 +307,22 @@ void PrefsDialog::pathChanged(QString const& path) } } +void PrefsDialog::plainTextChanged() +{ + PreferenceWidget const pref_widget(sender()); + + if (pref_widget.is()) + { + auto const* const plain_text_edit = pref_widget.as(); + + if (plain_text_edit->document()->isModified()) + { + prefs_.set(pref_widget.getPrefKey(), plain_text_edit->toPlainText()); + // we avoid using setPref() because the included refreshPref() call would reset the cursor while we're editing + } + } +} + /*** **** ***/ @@ -423,6 +451,7 @@ void PrefsDialog::initNetworkTab() linkWidgetToPref(ui_.enablePexCheck, Prefs::PEX_ENABLED); linkWidgetToPref(ui_.enableDhtCheck, Prefs::DHT_ENABLED); linkWidgetToPref(ui_.enableLpdCheck, Prefs::LPD_ENABLED); + linkWidgetToPref(ui_.defaultTrackersPlainTextEdit, Prefs::DEFAULT_TRACKERS); auto* cr = new ColumnResizer(this); cr->addLayout(ui_.incomingPeersSectionLayout); @@ -775,11 +804,15 @@ void PrefsDialog::refreshPref(int key) { QWidget* w(it.value()); + w->blockSignals(true); + if (!updateWidgetValue(w, key) && (key == Prefs::ENCRYPTION)) { auto* combo_box = qobject_cast(w); int const index = combo_box->findData(prefs_.getInt(key)); combo_box->setCurrentIndex(index); } + + w->blockSignals(false); } } diff --git a/qt/PrefsDialog.h b/qt/PrefsDialog.h index b43f56a2a..df8e823b8 100644 --- a/qt/PrefsDialog.h +++ b/qt/PrefsDialog.h @@ -34,6 +34,7 @@ private slots: void timeEditingFinished(); void lineEditingFinished(); void pathChanged(QString const& path); + void plainTextChanged(); void refreshPref(int key); void encryptionEdited(int); void altSpeedDaysEdited(int); diff --git a/qt/PrefsDialog.ui b/qt/PrefsDialog.ui index a5459fb19..8da1522cc 100644 --- a/qt/PrefsDialog.ui +++ b/qt/PrefsDialog.ui @@ -983,6 +983,26 @@ + + + + a list of default trackers to be added to new public torrents (and existing ones, after a reload) + + + Default Trackers: + + + + + + + QPlainTextEdit::NoWrap + + + + + + diff --git a/qt/Session.cc b/qt/Session.cc index 307ce34d2..b27b99728 100644 --- a/qt/Session.cc +++ b/qt/Session.cc @@ -155,6 +155,7 @@ void Session::updatePref(int key) case Prefs::BLOCKLIST_DATE: case Prefs::BLOCKLIST_ENABLED: case Prefs::BLOCKLIST_URL: + case Prefs::DEFAULT_TRACKERS: case Prefs::DHT_ENABLED: case Prefs::DOWNLOAD_QUEUE_ENABLED: case Prefs::DOWNLOAD_QUEUE_SIZE: diff --git a/tests/libtransmission/rpc-test.cc b/tests/libtransmission/rpc-test.cc index 7d948e9a8..8af1f26f8 100644 --- a/tests/libtransmission/rpc-test.cc +++ b/tests/libtransmission/rpc-test.cc @@ -94,7 +94,7 @@ TEST_F(RpcTest, sessionGet) EXPECT_TRUE(tr_variantDictFindDict(&response, TR_KEY_arguments, &args)); // what we expected - auto const expected_keys = std::array{ + auto const expected_keys = std::array{ TR_KEY_alt_speed_down, TR_KEY_alt_speed_enabled, TR_KEY_alt_speed_time_begin, @@ -109,6 +109,7 @@ TEST_F(RpcTest, sessionGet) TR_KEY_blocklist_url, TR_KEY_cache_size_mb, TR_KEY_config_dir, + TR_KEY_default_trackers, TR_KEY_dht_enabled, TR_KEY_download_dir, TR_KEY_download_dir_free_space, diff --git a/web/src/prefs-dialog.js b/web/src/prefs-dialog.js index 55da8b893..f0d51e0c0 100644 --- a/web/src/prefs-dialog.js +++ b/web/src/prefs-dialog.js @@ -63,6 +63,10 @@ export class PrefsDialog extends EventTarget { } static _getValue(e) { + if (e.tagName === 'TEXTAREA') { + return e.value; + } + switch (e.type) { case 'checkbox': case 'radio': @@ -118,6 +122,15 @@ export class PrefsDialog extends EventTarget { const n = Formatter.number(value); element.innerHTML = `Blocklist has ${n} rules`; setTextContent(this.elements.peers.blocklist_update_button, 'Update'); + } else if (element.tagName === 'TEXTAREA') { + if ( + // eslint-disable-next-line eqeqeq + element.value != value && + element !== document.activeElement + ) { + element.value = value; + element.dispatchEvent(new Event('change')); + } } else { switch (element.type) { case 'checkbox': @@ -646,7 +659,25 @@ export class PrefsDialog extends EventTarget { root.append(cal.root); const utp_check = cal.check; + label = document.createElement('div'); + label.textContent = 'Default trackers'; + label.classList.add('section-label'); + root.append(label); + + label = document.createElement('label'); + label.textContent = + '(added to new public torrents and to existing ones on reload):'; + root.append(label); + + const textarea = document.createElement('textarea'); + textarea.dataset.key = 'default-trackers'; + textarea.id = 'default-trackers'; + label.setAttribute('for', textarea.id); + root.append(textarea); + const default_trackers_textarea = textarea; + return { + default_trackers_textarea, port_forwarding_check, port_input, port_status_label, @@ -714,6 +745,8 @@ export class PrefsDialog extends EventTarget { console.trace(`unhandled input: ${element.type}`); break; } + } else if (element.tagName === 'TEXTAREA') { + element.addEventListener('change', on_change); } } }; diff --git a/web/style/transmission-app.scss b/web/style/transmission-app.scss index acdc7f50a..a670c8e15 100644 --- a/web/style/transmission-app.scss +++ b/web/style/transmission-app.scss @@ -762,6 +762,10 @@ $popup-top: 61px; // TODO: ugly that this is hardcoded grid-column: span 2; } + #default-trackers { + height: 300px; + } + .blocklist-size-label, .blocklist-update-button, .port-status {