mirror of
https://git.tt-rss.org/git/tt-rss.git
synced 2025-12-13 12:05:55 +00:00
1. better debugging output 2. fix incorrect default values being used sometimes 3. remove special workaround for equal titles because trgm extension seems to be working properly for those now (tested on postgres 11) 4. code cleanup
363 lines
10 KiB
PHP
363 lines
10 KiB
PHP
<?php
|
|
class Af_Psql_Trgm extends Plugin {
|
|
|
|
/* @var PluginHost $host */
|
|
private $host;
|
|
private $default_similarity = 0.75;
|
|
private $default_min_length = 32;
|
|
|
|
function about() {
|
|
return array(1.0,
|
|
"Marks similar articles as read (requires pg_trgm)",
|
|
"fox");
|
|
}
|
|
|
|
function save() {
|
|
$similarity = (float) $_POST["similarity"];
|
|
$min_title_length = (int) $_POST["min_title_length"];
|
|
$enable_globally = checkbox_to_sql_bool($_POST["enable_globally"]);
|
|
|
|
if ($similarity < 0) $similarity = 0;
|
|
if ($similarity > 1) $similarity = 1;
|
|
|
|
if ($min_title_length < 0) $min_title_length = 0;
|
|
|
|
$similarity = sprintf("%.2f", $similarity);
|
|
|
|
$this->host->set($this, "similarity", $similarity);
|
|
$this->host->set($this, "min_title_length", $min_title_length);
|
|
$this->host->set($this, "enable_globally", $enable_globally);
|
|
|
|
echo T_sprintf("Data saved (%s, %d)", $similarity, $enable_globally);
|
|
}
|
|
|
|
function init($host) {
|
|
$this->host = $host;
|
|
|
|
$host->add_hook($host::HOOK_ARTICLE_FILTER, $this);
|
|
$host->add_hook($host::HOOK_PREFS_TAB, $this);
|
|
$host->add_hook($host::HOOK_PREFS_EDIT_FEED, $this);
|
|
$host->add_hook($host::HOOK_PREFS_SAVE_FEED, $this);
|
|
$host->add_hook($host::HOOK_ARTICLE_BUTTON, $this);
|
|
}
|
|
|
|
function get_js() {
|
|
return file_get_contents(__DIR__ . "/init.js");
|
|
}
|
|
|
|
function showrelated() {
|
|
$id = (int) $_REQUEST['param'];
|
|
$owner_uid = $_SESSION["uid"];
|
|
|
|
$sth = $this->pdo->prepare("SELECT title FROM ttrss_entries, ttrss_user_entries
|
|
WHERE ref_id = id AND id = ? AND owner_uid = ?");
|
|
$sth->execute([$id, $owner_uid]);
|
|
|
|
if ($row = $sth->fetch()) {
|
|
|
|
$title = $row['title'];
|
|
|
|
print "<p>$title</p>";
|
|
|
|
$sth = $this->pdo->prepare("SELECT ttrss_entries.id AS id,
|
|
feed_id,
|
|
ttrss_entries.title AS title,
|
|
updated, link,
|
|
ttrss_feeds.title AS feed_title,
|
|
SIMILARITY(ttrss_entries.title, ?) AS sm
|
|
FROM
|
|
ttrss_entries, ttrss_user_entries LEFT JOIN ttrss_feeds ON (ttrss_feeds.id = feed_id)
|
|
WHERE
|
|
ttrss_entries.id = ref_id AND
|
|
ttrss_user_entries.owner_uid = ? AND
|
|
ttrss_entries.id != ? AND
|
|
date_entered >= NOW() - INTERVAL '2 weeks'
|
|
ORDER BY
|
|
sm DESC, date_entered DESC
|
|
LIMIT 10");
|
|
|
|
$sth->execute([$title, $owner_uid, $id]);
|
|
|
|
print "<ul class='panel panel-scrollable'>";
|
|
|
|
while ($line = $sth->fetch()) {
|
|
print "<li style='display : flex'>";
|
|
print "<i class='material-icons'>bookmark_outline</i>";
|
|
|
|
$sm = sprintf("%.2f", $line['sm']);
|
|
$article_link = htmlspecialchars($line["link"]);
|
|
|
|
print "<div style='flex-grow : 2'>";
|
|
|
|
print " <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"$article_link\">".
|
|
$line["title"]."</a>";
|
|
|
|
print " (<a href=\"#\" onclick=\"Feeds.open({feed:".$line["feed_id"]."})\">".
|
|
htmlspecialchars($line["feed_title"])."</a>)";
|
|
|
|
print " — $sm";
|
|
|
|
print "</div>";
|
|
|
|
print "<div style='text-align : right' class='text-muted'>" . TimeHelper::smart_date_time(strtotime($line["updated"])) . "</div>";
|
|
|
|
print "</li>";
|
|
}
|
|
|
|
print "</ul>";
|
|
|
|
}
|
|
|
|
print "<footer class='text-center'>";
|
|
print "<button dojoType='dijit.form.Button' onclick=\"dijit.byId('trgmRelatedDlg').hide()\">".__('Close this window')."</button>";
|
|
print "</footer>";
|
|
|
|
|
|
}
|
|
|
|
function hook_article_button($line) {
|
|
return "<i style=\"cursor : pointer\" class='material-icons'
|
|
onclick=\"Plugins.Psql_Trgm.showRelated(".$line["id"].")\"
|
|
title='".__('Show related articles')."'>bookmark_outline</i>";
|
|
}
|
|
|
|
function hook_prefs_tab($args) {
|
|
if ($args != "prefFeeds") return;
|
|
|
|
print "<div dojoType=\"dijit.layout.AccordionPane\"
|
|
title=\"<i class='material-icons'>extension</i> ".__('Mark similar articles as read (af_psql_trgm)')."\">";
|
|
|
|
if (DB_TYPE != "pgsql") {
|
|
print_error("Database type not supported.");
|
|
} else {
|
|
|
|
$res = $this->pdo->query("select 'similarity'::regproc");
|
|
|
|
if (!$res || !$res->fetch()) {
|
|
print_error("pg_trgm extension not found.");
|
|
}
|
|
|
|
$similarity = $this->host->get($this, "similarity", $this->default_similarity);
|
|
$min_title_length = $this->host->get($this, "min_title_length", $this->default_min_length);
|
|
$enable_globally = $this->host->get($this, "enable_globally");
|
|
|
|
print "<form dojoType=\"dijit.form.Form\">";
|
|
|
|
print "<script type=\"dojo/method\" event=\"onSubmit\" args=\"evt\">
|
|
evt.preventDefault();
|
|
if (this.validate()) {
|
|
console.log(dojo.objectToQuery(this.getValues()));
|
|
new Ajax.Request('backend.php', {
|
|
parameters: dojo.objectToQuery(this.getValues()),
|
|
onComplete: function(transport) {
|
|
Notify.info(transport.responseText);
|
|
}
|
|
});
|
|
//this.reset();
|
|
}
|
|
</script>";
|
|
|
|
print_hidden("op", "pluginhandler");
|
|
print_hidden("method", "save");
|
|
print_hidden("plugin", "af_psql_trgm");
|
|
|
|
print "<h2>" . __("Global settings") . "</h2>";
|
|
|
|
print_notice("Enable for specific feeds in the feed editor.");
|
|
|
|
print "<fieldset>";
|
|
|
|
print "<label>" . __("Minimum similarity:") . "</label> ";
|
|
print "<input dojoType=\"dijit.form.NumberSpinner\"
|
|
placeholder=\"0.75\" id='psql_trgm_similarity'
|
|
required=\"1\" name=\"similarity\" value=\"$similarity\">";
|
|
|
|
print "<div dojoType='dijit.Tooltip' connectId='psql_trgm_similarity' position='below'>" .
|
|
__("PostgreSQL trigram extension returns string similarity as a floating point number (0-1). Setting it too low might produce false positives, zero disables checking.") .
|
|
"</div>";
|
|
|
|
print "</fieldset><fieldset>";
|
|
|
|
print "<label>" . __("Minimum title length:") . "</label> ";
|
|
print "<input dojoType=\"dijit.form.NumberSpinner\"
|
|
placeholder=\"32\"
|
|
required=\"1\" name=\"min_title_length\" value=\"$min_title_length\">";
|
|
|
|
print "</fieldset><fieldset>";
|
|
|
|
print "<label class='checkbox'>";
|
|
print_checkbox("enable_globally", $enable_globally);
|
|
print " " . __("Enable for all feeds:");
|
|
print "</label>";
|
|
|
|
print "</fieldset>";
|
|
|
|
print_button("submit", __("Save"), "class='alt-primary'");
|
|
print "</form>";
|
|
|
|
/* cleanup */
|
|
$enabled_feeds = $this->filter_unknown_feeds(
|
|
$this->get_stored_array("enabled_feeds"));
|
|
|
|
$this->host->set($this, "enabled_feeds", $enabled_feeds);
|
|
|
|
if (count($enabled_feeds) > 0) {
|
|
print "<h3>" . __("Currently enabled for (click to edit):") . "</h3>";
|
|
|
|
print "<ul class=\"panel panel-scrollable list list-unstyled\">";
|
|
foreach ($enabled_feeds as $f) {
|
|
print "<li>" .
|
|
"<i class='material-icons'>rss_feed</i> <a href='#'
|
|
onclick='CommonDialogs.editFeed($f)'>" .
|
|
Feeds::getFeedTitle($f) . "</a></li>";
|
|
}
|
|
print "</ul>";
|
|
}
|
|
}
|
|
|
|
print "</div>";
|
|
}
|
|
|
|
function hook_prefs_edit_feed($feed_id) {
|
|
print "<header>".__("Similarity (af_psql_trgm)")."</header>";
|
|
print "<section>";
|
|
|
|
$enabled_feeds = $this->get_stored_array("enabled_feeds");
|
|
$checked = in_array($feed_id, $enabled_feeds) ? "checked" : "";
|
|
|
|
print "<fieldset>";
|
|
|
|
print "<label class='checkbox'><input dojoType='dijit.form.CheckBox' type='checkbox' id='trgm_similarity_enabled'
|
|
name='trgm_similarity_enabled' $checked> ".__('Mark similar articles as read')."</label>";
|
|
|
|
print "</fieldset>";
|
|
|
|
print "</section>";
|
|
}
|
|
|
|
function hook_prefs_save_feed($feed_id) {
|
|
$enabled_feeds = $this->get_stored_array("enabled_feeds");
|
|
|
|
$enable = checkbox_to_sql_bool($_POST["trgm_similarity_enabled"]);
|
|
$key = array_search($feed_id, $enabled_feeds);
|
|
|
|
if ($enable) {
|
|
if ($key === false) {
|
|
array_push($enabled_feeds, $feed_id);
|
|
}
|
|
} else {
|
|
if ($key !== false) {
|
|
unset($enabled_feeds[$key]);
|
|
}
|
|
}
|
|
|
|
$this->host->set($this, "enabled_feeds", $enabled_feeds);
|
|
}
|
|
|
|
function hook_article_filter($article) {
|
|
|
|
if (DB_TYPE != "pgsql") return $article;
|
|
|
|
$res = $this->pdo->query("select 'similarity'::regproc");
|
|
if (!$res || !$res->fetch()) return $article;
|
|
|
|
$enable_globally = $this->host->get($this, "enable_globally");
|
|
|
|
if (!$enable_globally &&
|
|
!in_array($article["feed"]["id"],
|
|
$this->get_stored_array("enabled_feeds"))) {
|
|
|
|
return $article;
|
|
}
|
|
|
|
$similarity = (float) $this->host->get($this, "similarity", $this->default_similarity);
|
|
|
|
if ($similarity < 0.01) {
|
|
Debug::log("af_psql_trgm: similarity is set too low ($similarity)", Debug::$LOG_EXTENDED);
|
|
return $article;
|
|
}
|
|
|
|
$min_title_length = (int) $this->host->get($this, "min_title_length", $this->default_min_length);
|
|
|
|
if (mb_strlen($article["title"]) < $min_title_length) {
|
|
Debug::log("af_psql_trgm: article title is too short (min: $min_title_length)", Debug::$LOG_EXTENDED);
|
|
return $article;
|
|
}
|
|
|
|
$owner_uid = $article["owner_uid"];
|
|
$entry_guid = $article["guid_hashed"];
|
|
$title_escaped = $article["title"];
|
|
|
|
// trgm does not return similarity=1 for completely equal strings
|
|
// this seems to be no longer the case (fixed in upstream?)
|
|
|
|
/* $sth = $this->pdo->prepare("SELECT COUNT(id) AS nequal
|
|
FROM ttrss_entries, ttrss_user_entries WHERE ref_id = id AND
|
|
date_entered >= NOW() - interval '3 days' AND
|
|
title = ? AND
|
|
guid != ? AND
|
|
owner_uid = ?");
|
|
$sth->execute([$title_escaped, $entry_guid, $owner_uid]);
|
|
|
|
$row = $sth->fetch();
|
|
$nequal = $row['nequal'];
|
|
|
|
Debug::log("af_psql_trgm: num equals: $nequal", Debug::$LOG_EXTENDED);
|
|
|
|
if ($nequal != 0) {
|
|
$article["force_catchup"] = true;
|
|
return $article;
|
|
} */
|
|
|
|
$sth = $this->pdo->prepare("SELECT MAX(SIMILARITY(title, ?)) AS ms
|
|
FROM ttrss_entries, ttrss_user_entries WHERE ref_id = id AND
|
|
date_entered >= NOW() - interval '1 day' AND
|
|
guid != ? AND
|
|
owner_uid = ?");
|
|
$sth->execute([$title_escaped, $entry_guid, $owner_uid]);
|
|
|
|
$row = $sth->fetch();
|
|
$similarity_result = $row['ms'];
|
|
|
|
Debug::log("af_psql_trgm: similarity result for $title_escaped: $similarity_result", Debug::$LOG_EXTENDED);
|
|
|
|
if ($similarity_result >= $similarity) {
|
|
Debug::log("af_psql_trgm: marking article as read ($similarity_result >= $similarity)", Debug::$LOG_EXTENDED);
|
|
|
|
$article["force_catchup"] = true;
|
|
}
|
|
|
|
return $article;
|
|
}
|
|
|
|
function api_version() {
|
|
return 2;
|
|
}
|
|
|
|
private function filter_unknown_feeds($enabled_feeds) {
|
|
$tmp = array();
|
|
|
|
foreach ($enabled_feeds as $feed) {
|
|
|
|
$sth = $this->pdo->prepare("SELECT id FROM ttrss_feeds WHERE id = ? AND owner_uid = ?");
|
|
$sth->execute([$feed, $_SESSION['uid']]);
|
|
|
|
if ($row = $sth->fetch()) {
|
|
array_push($tmp, $feed);
|
|
}
|
|
}
|
|
|
|
return $tmp;
|
|
}
|
|
|
|
private function get_stored_array($name) {
|
|
$tmp = $this->host->get($this, $name);
|
|
|
|
if (!is_array($tmp)) $tmp = [];
|
|
|
|
return $tmp;
|
|
}
|
|
|
|
|
|
}
|