Răsfoiți Sursa

Add read status hook for entries (#8995)

- Add a new `EntriesRead` / `entries_read` hook for extensions.
- Trigger the hook when `markRead()` changes entry read/unread state for explicit entry IDs.
- Cover both the default DAO path and the SQLite/PGSQL DAO path.
- Add Minz hook tests for the new hook signature and argument passing.

## Why

Extensions can already react to favorite/bookmark changes through `EntriesFavorite`. This adds the matching read/unread hook requested in #4051 for extensions that need to collect stats or react to manual read state changes.

Closes #4051
Jam Balaya 10 ore în urmă
părinte
comite
310bcb5e90

+ 8 - 1
app/Models/EntryDAO.php

@@ -538,6 +538,9 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo {
 			if (($affected > 0) && (!$this->updateCacheUnreads(null, null))) {
 				return false;
 			}
+			if ($affected > 0) {
+				Minz_ExtensionManager::callHook(Minz_HookType::EntriesRead, $ids, $is_read);
+			}
 			return $affected;
 		} else {
 			FreshRSS_UserDAO::touch();
@@ -555,7 +558,11 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo {
 				$stm->bindValue(':id', $ids, PDO::PARAM_STR) &&	// TODO: Test PDO::PARAM_INT on 32-bit platform
 				$stm->bindValue(':old_is_read', $is_read ? 0 : 1, PDO::PARAM_INT) &&
 				$stm->execute()) {
-				return $stm->rowCount();
+				$affected = $stm->rowCount();
+				if ($affected > 0) {
+					Minz_ExtensionManager::callHook(Minz_HookType::EntriesRead, [$ids], $is_read);
+				}
+				return $affected;
 			} else {
 				$info = $stm === false ? $this->pdo->errorInfo() : $stm->errorInfo();
 				/** @var array{0:string,1:int,2:string} $info */

+ 3 - 0
app/Models/EntryDAOSQLite.php

@@ -164,6 +164,9 @@ class FreshRSS_EntryDAOSQLite extends FreshRSS_EntryDAO {
 				}
 			}
 			$this->pdo->commit();
+			if ($affected > 0) {
+				Minz_ExtensionManager::callHook(Minz_HookType::EntriesRead, [$ids], $is_read);
+			}
 			return $affected;
 		}
 	}

+ 2 - 0
docs/en/developers/03_Backend/05_Extensions.md

@@ -181,6 +181,8 @@ Example response for a `query_icon_info` request:
 * `Minz_HookType::CustomFaviconHash` (`function(FreshRSS_Feed $feed): string | null`): Enables the modification of custom favicon hashes by returning params from the hook function. The hook should check if the `customFaviconExt` attribute of `$feed` is set to the extension's name before returning a custom value. Otherwise, the return value should be null.
 * `Minz_HookType::EntriesFavorite` (`function(array $ids, bool $is_favorite): void`):
 	will be executed when some entries are marked or unmarked as favorites (starred)
+* `Minz_HookType::EntriesRead` (`function(array $ids, bool $is_read): void`):
+	will be executed when some entries are marked or unmarked as read
 * `Minz_HookType::EntryAutoRead` (`function(FreshRSS_Entry $entry, string $why): void`): Triggered when an entry is automatically marked as read. The *why* parameter supports the rules {`filter`, `upon_reception`, `same_title_in_feed`, `same_guid_in_category`}.
 * `Minz_HookType::EntryAutoUnread` (`function(FreshRSS_Entry $entry, string $why): void`): Triggered when an entry is automatically marked as unread. The *why* parameter supports the rules {`updated_article`}.
 * `Minz_HookType::EntryBeforeDisplay` (`function($entry) -> Entry | null`): will be executed every time an entry is rendered. The entry itself (instance of FreshRSS\_Entry) will be passed as parameter.

+ 2 - 0
lib/Minz/HookType.php

@@ -9,6 +9,7 @@ enum Minz_HookType: string {
 	case CustomFaviconBtnUrl = 'custom_favicon_btn_url';	// function(FreshRSS_Feed $feed): string | null
 	case CustomFaviconHash = 'custom_favicon_hash';	// function(FreshRSS_Feed $feed): string | null
 	case EntriesFavorite = 'entries_favorite';	// function(array $ids, bool $is_favorite): void
+	case EntriesRead = 'entries_read';	// function(array $ids, bool $is_read): void
 	case EntryAutoRead = 'entry_auto_read';	// function(FreshRSS_Entry $entry, string $why): void
 	case EntryAutoUnread = 'entry_auto_unread';	// function(FreshRSS_Entry $entry, string $why): void
 	case EntryBeforeAdd = 'entry_before_add';	// function(FreshRSS_Entry $entry) -> FreshRSS_Entry | null
@@ -62,6 +63,7 @@ enum Minz_HookType: string {
 			case self::CustomFaviconBtnUrl:
 			case self::CustomFaviconHash:
 			case self::EntriesFavorite:
+			case self::EntriesRead:
 			case self::EntryAutoRead:
 			case self::EntryAutoUnread:
 			case self::SimplepieAfterInit:

+ 28 - 0
tests/lib/Minz/HookTypeTest.php

@@ -0,0 +1,28 @@
+<?php
+declare(strict_types=1);
+use PHPUnit\Framework\TestCase;
+
+class HookTypeTest extends TestCase
+{
+	public static function testEntriesReadHookUsesPassArgumentsSignature(): void {
+		self::assertSame(Minz_HookSignature::PassArguments, Minz_HookType::EntriesRead->signature());
+	}
+
+	public static function testEntriesReadHookPassesIdsAndReadState(): void {
+		$actual_ids = [];
+		$actual_is_read = null;
+
+		Minz_ExtensionManager::addHook(
+			Minz_HookType::EntriesRead,
+			static function (array $ids, bool $is_read) use (&$actual_ids, &$actual_is_read): void {
+				$actual_ids = $ids;
+				$actual_is_read = $is_read;
+			}
+		);
+
+		Minz_ExtensionManager::callHook(Minz_HookType::EntriesRead, ['123', '456'], false);
+
+		self::assertSame(['123', '456'], $actual_ids);
+		self::assertFalse($actual_is_read);
+	}
+}