Explorar o código

Add feeds_after_actualize hook (#9438)

Extensions that process new articles in bulk (summaries, exports,
notifications) have no way to react to the end of a refresh:
freshrss_user_maintenance runs before the feeds are fetched, and new
articles stay in the temporary table until the whole loop is done.

The new FeedsAfterActualize hook runs once the new articles are
committed, with the refreshed feeds whose articles changed and the
number of new articles: after a cron or web refresh (actualizeAction,
including the commit-only request of the web interface) and after
actualizeFeedsAndCommit (CLI, API, WebSub, feed added).

Co-authored-by: ChrisB85 <792899+ChrisB85@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
ChrisB85 hai 20 horas
pai
achega
2ebfff6e7d

+ 7 - 1
app/Controllers/feedController.php

@@ -959,6 +959,7 @@ class FreshRSS_feed_Controller extends FreshRSS_ActionController {
 		if ($entryDAO->inTransaction()) {
 			$entryDAO->commit();
 		}
+		Minz_ExtensionManager::callHook(Minz_HookType::FeedsAfterActualize, $feedsCacheToRefresh, $nbNewArticles);
 		if (rand(0, 30) === 1) {	// Remove old cache once in a while
 			cleanCache(CLEANCACHE_HOURS);
 		}
@@ -985,9 +986,11 @@ class FreshRSS_feed_Controller extends FreshRSS_ActionController {
 		if ($id === -1 && !$noCommit) {	//Special request only to commit & refresh DB cache
 			$nbUpdatedFeeds = 0;
 			$feed = null;
-			FreshRSS_feed_Controller::commitNewEntries();
+			$nbNewArticles = FreshRSS_feed_Controller::commitNewEntries();
 			$feedDAO = FreshRSS_Factory::createFeedDao();
 			$feedDAO->updateCachedValues();
+			// The refreshed feeds are not known here: the web interface refreshed them in earlier requests without commit
+			Minz_ExtensionManager::callHook(Minz_HookType::FeedsAfterActualize, [], $nbNewArticles);
 		} else {
 			if (!$noCommit) {
 				$databaseDAO = FreshRSS_Factory::createDatabaseDAO();
@@ -1024,6 +1027,9 @@ class FreshRSS_feed_Controller extends FreshRSS_ActionController {
 			if ($entryDAO->inTransaction()) {
 				$entryDAO->commit();
 			}
+			if (!$noCommit) {
+				Minz_ExtensionManager::callHook(Minz_HookType::FeedsAfterActualize, $feedsCacheToRefresh, $nbNewArticles);
+			}
 		}
 
 		if (Minz_Request::paramBoolean('ajax')) {

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

@@ -196,6 +196,11 @@ Example response for a `query_icon_info` request:
 * `Minz_HookType::EntryBeforeUpdate` (`function($entry) -> Entry | null`): will be executed when a feed is refreshed and just before an entry is updated in the database. Useful for reading the final state of the entry after filter actions have been applied. The updated entry (instance of FreshRSS\_Entry) will be passed as parameter.
 * `Minz_HookType::FeedBeforeActualize` (`function($feed) -> Feed | null`): will be executed when a feed is updated. The feed (instance of FreshRSS\_Feed) will be passed as parameter.
 * `Minz_HookType::FeedBeforeInsert` (`function($feed) -> Feed | null`): will be executed when a new feed is imported into the database. The new feed (instance of FreshRSS\_Feed) will be passed as parameter.
+* `Minz_HookType::FeedsAfterActualize` (`function(array $feeds, int $nbNewArticles): void`):
+	will be executed at the end of a refresh (cron, web interface, API, CLI, WebSub), once the new articles are committed
+	and therefore visible to queries and the API. `$feeds` lists the refreshed feeds whose articles changed,
+	`$nbNewArticles` counts the new articles. `$feeds` is empty when the web interface commits the articles of feeds it refreshed
+	in earlier requests. Useful for extensions that process new articles in bulk.
 * `Minz_HookType::FeedsListBeforeActualize` (`function(array<FreshRSS_Feed> $feedList) -> array | null`): will be executed before FreshRSS tries to update feeds. The list of feeds (array of `FreshRSS_Feed`) to update will be passed as a parameter. Useful for modifying the order in which the feeds will be updated.
 * `Minz_HookType::FreshrssInit` (`function() -> none`): will be executed at the end of the initialization of FreshRSS, useful to initialize components or to do additional access checks.
 * `Minz_HookType::FreshrssUserMaintenance` (`function() -> none`): will be executed for each user during the `actualize_script`, useful to run some maintenance tasks on the user.

+ 2 - 0
lib/Minz/HookType.php

@@ -19,6 +19,7 @@ enum Minz_HookType: string {
 	case EntryBeforeUpdate = 'entry_before_update';	// function(FreshRSS_Entry $entry) -> FreshRSS_Entry | null
 	case FeedBeforeActualize = 'feed_before_actualize';	// function(FreshRSS_Feed $feed) -> FreshRSS_Feed | null
 	case FeedBeforeInsert = 'feed_before_insert';	// function(FreshRSS_Feed $feed) -> FreshRSS_Feed | null
+	case FeedsAfterActualize = 'feeds_after_actualize';	// function(array $feeds, int $nbNewArticles): void
 	case FeedsListBeforeActualize = 'feeds_list_before_actualize';	// function(array $feedsList) -> array
 	case FreshrssInit = 'freshrss_init';	// function() -> none
 	case FreshrssUserMaintenance = 'freshrss_user_maintenance';	// function() -> none
@@ -68,6 +69,7 @@ enum Minz_HookType: string {
 			case self::EntriesRead:
 			case self::EntryAutoRead:
 			case self::EntryAutoUnread:
+			case self::FeedsAfterActualize:
 			case self::SimplepieAfterInit:
 			case self::SimplepieBeforeInit:
 				return Minz_HookSignature::PassArguments;

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

@@ -25,4 +25,27 @@ class HookTypeTest extends TestCase
 		self::assertSame(['123', '456'], $actual_ids);
 		self::assertFalse($actual_is_read);
 	}
+
+	public static function testFeedsAfterActualizeHookUsesPassArgumentsSignature(): void {
+		self::assertSame(Minz_HookSignature::PassArguments, Minz_HookType::FeedsAfterActualize->signature());
+	}
+
+	public static function testFeedsAfterActualizeHookPassesFeedsAndNewArticleCount(): void {
+		$actual_feeds = null;
+		$actual_count = null;
+		$feed = new FreshRSS_Feed('https://example.net/feed.xml');
+
+		Minz_ExtensionManager::addHook(
+			Minz_HookType::FeedsAfterActualize,
+			static function (array $feeds, int $nbNewArticles) use (&$actual_feeds, &$actual_count): void {
+				$actual_feeds = $feeds;
+				$actual_count = $nbNewArticles;
+			}
+		);
+
+		Minz_ExtensionManager::callHook(Minz_HookType::FeedsAfterActualize, [$feed], 3);
+
+		self::assertSame([$feed], $actual_feeds);
+		self::assertSame(3, $actual_count);
+	}
 }