Commit 08500547 authored by Jano's avatar Jano

Make it possible for bell notifications to expire

Some bell notifications need to be removed at a specific time without
an event happening at this specific time. Others need to be updated
then, as attributes change, for example their count.

This commit implements a call in the listBells() method of the
BellGateway class, that calls the newly introduced BellUpdateTrigger to
update expired bells before they are delivered to the client.

This implementation stands in contrast to our immediate bell updates via
the webSocket, but the expiration updates would require a cron job or
a tricky client side implementation to be triggered immediately, that's
why it's triggered every time bells are fetched from the database now,
which means that it will happen after every page reloading and after
anything else that triggers an update of the bells.

The BellUpdateTrigger is a service, that all classes that need to update
their bells when they expire, should subscribe to. For that, they need to
implement the updateExpiredBells function of the BellUpdaterInterface.
After they subscribed, their implementations of the updateExpiredBells()
function will be called every time the someone calls the
BellUpdateTrigger::triggerUpdate() function (which, as already
mentioned, currently happens before bells are fetched from the
database).

This implementation allows to easily change the trigger for
the updates, for example, we could easily move it to a cronjob or to an
Xhr function that gets called by the frontend. It also makes it easily
possible for every class to become a bell updater and send bells with
an expiration date to update them later.
parent ed7a9f59
ALTER TABLE `fs_bell` ADD `expiration` DATE NULL DEFAULT NULL AFTER `closeable`;
\ No newline at end of file
......@@ -12,18 +12,24 @@ class BellGateway extends BaseGateway
* @var WebSocketSender
*/
private $webSocketSender;
/**
* @var BellUpdateTrigger
*/
private $bellUpdateTrigger;
public function __construct(Database $db, WebSocketSender $webSocketSender)
public function __construct(Database $db, WebSocketSender $webSocketSender, BellUpdateTrigger $bellUpdateTrigger)
{
parent::__construct($db);
$this->webSocketSender = $webSocketSender;
$this->bellUpdateTrigger = $bellUpdateTrigger;
}
/**
* @param int|int[] $foodsaver_ids
* @param string[] $link_attributes
* @param string[] $vars
* @param int|null $expiration A unix timestamp that defines when the time since when the bell will be outdated and not shown anymore - null means it doesn't expire
* @param int|null $timestamp A unix timestamp for the bell's time - null means current date and time
*/
public function addBell(
......@@ -35,6 +41,7 @@ class BellGateway extends BaseGateway
array $vars,
string $identifier = '',
int $closeable = 1,
?int $expiration = null,
?int $timestamp = null
): void {
if (!is_array($foodsaver_ids)) {
......@@ -63,7 +70,8 @@ class BellGateway extends BaseGateway
'icon' => strip_tags($icon),
'identifier' => strip_tags($identifier),
'time' => date('Y-m-d H:i:s', $timestamp),
'closeable' => $closeable
'closeable' => $closeable,
'expiration' => date('Y-m-d H:i:s', $expiration)
]
);
......@@ -79,48 +87,24 @@ class BellGateway extends BaseGateway
}
/**
* @param string[] $link_attributes
* @param string[] $vars
* @param int|null $timestamp A unix timestamp for the bells time - null means current date and time
* @param array $data - the data to be updated. $data['var'] and data['attr'] must not be serialized.
*/
public function updateBell(
int $bellId,
string $title,
string $body,
string $icon,
array $link_attributes,
array $vars,
string $identifier = '',
int $closeable = 1,
?int $timestamp = null,
bool $setUnseen = false
): void {
if ($link_attributes !== false) {
$link_attributes = serialize($link_attributes);
public function updateBell(int $bellId, array $data, bool $setUnseen = false, bool $updateClients = true): void
{
if (isset($data['attr'])) {
$data['attr'] = serialize($data['attr']);
}
if ($vars !== false) {
$vars = serialize($vars);
if (isset($data['vars'])) {
$data['vars'] = serialize($data['vars']);
}
if ($timestamp === null) {
$timestamp = time();
if (isset($data['timestamp'])) {
$data['time'] = date('Y-m-d H:i:s', $data['timestamp']);
unset($data['timestamp']);
}
$this->db->update(
'fs_bell',
[
'name' => strip_tags($title),
'body' => strip_tags($body),
'vars' => strip_tags($vars),
'attr' => strip_tags($link_attributes),
'icon' => strip_tags($icon),
'identifier' => strip_tags($identifier),
'time' => date('Y-m-d H:i:s', $timestamp),
'closeable' => $closeable
],
['id' => $bellId]
);
$this->db->update('fs_bell', $data, ['id' => $bellId]);
$foodsaverIds = $this->db->fetchAllValuesByCriteria('fs_foodsaver_has_bell', 'foodsaver_id', ['bell_id' => $bellId]);
......@@ -128,7 +112,9 @@ class BellGateway extends BaseGateway
$this->db->update('fs_foodsaver_has_bell', ['seen' => 0], ['foodsaver_id' => $foodsaverIds, 'bell_id' => $bellId]);
}
$this->updateMultipleFoodsaversClients($foodsaverIds);
if ($updateClients) {
$this->updateMultipleFoodsaversClients($foodsaverIds);
}
}
/**
......@@ -141,6 +127,8 @@ class BellGateway extends BaseGateway
*/
public function listBells($fsId, $limit = '')
{
$this->bellUpdateTrigger->triggerUpdate();
if ($limit !== '') {
$limit = ' LIMIT 0,' . (int)$limit;
}
......@@ -174,18 +162,7 @@ class BellGateway extends BaseGateway
';
if ($bells = $this->db->fetchAll($stm, [':foodsaver_id' => $fsId])
) {
$ids = array();
foreach ($bells as $i => $iValue) {
$ids[] = (int)$bells[$i]['id'];
if (!empty($bells[$i]['vars'])) {
$bells[$i]['vars'] = unserialize($bells[$i]['vars'], array('allowed_classes' => false));
}
if (!empty($bells[$i]['attr'])) {
$bells[$i]['attr'] = unserialize($bells[$i]['attr'], array('allowed_classes' => false));
}
}
$bells = $this->unserializeBells($bells);
return $bells;
}
......@@ -193,9 +170,42 @@ class BellGateway extends BaseGateway
return [];
}
/**
* @param string $identifier - can contain SQL wildcards
*
* @return int - id of the bell
*/
public function getOneByIdentifier(string $identifier): int
{
return $this->db->fetchValueByCriteria('fs_bell', 'id', ['identifier' => $identifier]);
return $this->db->fetchValueByCriteria('fs_bell', 'id', ['identifier like' => $identifier]);
}
/**
* @param string $identifier - can contain SQL wildcards
*
* @return array - [index => ['id', 'name', 'body', 'vars', 'attr', 'icon', 'identifier', 'time', 'time_ts', 'closable']
*/
public function getExpiredByIdentifier(string $identifier): array
{
$bells = $this->db->fetchAll('
SELECT
`id`,
`name`,
`body`,
`vars`,
`attr`,
`icon`,
`identifier`,
`time`,
UNIX_TIMESTAMP(`time`) AS time_ts,
`closeable`
FROM `fs_bell`
WHERE `identifier` LIKE :identifier
AND `expiration` < NOW()',
[':identifier' => $identifier]
);
return $this->unserializeBells($bells);
}
public function bellWithIdentifierExists(string $identifier): bool
......@@ -243,4 +253,24 @@ class BellGateway extends BaseGateway
{
$this->webSocketSender->sendSockMulti($foodsaverIds, 'bell', 'update', []);
}
/**
* @param array $bells - 2D-array with bell data, needs indexes []['vars'] and []['attr'] to contain serialized data
*
* @return array - array with the same structure as the input, but with unserialized []['vars'] and []['attr']
*/
private function unserializeBells(array $bells): array
{
foreach ($bells as $i => $iValue) {
if (!empty($bells[$i]['vars'])) {
$bells[$i]['vars'] = unserialize($bells[$i]['vars'], array('allowed_classes' => false));
}
if (!empty($bells[$i]['attr'])) {
$bells[$i]['attr'] = unserialize($bells[$i]['attr'], array('allowed_classes' => false));
}
}
return $bells;
}
}
<?php
namespace Foodsharing\Modules\Bell;
/**
* This class triggers bell updates. If a class needs to update its bells when they get outdated (expire), it
* can subscribe to this bellUpdater to be called when a bell is expired. For this, it needs to implement
* the BellUpdaterInterface.
*/
class BellUpdateTrigger
{
/**
* @var BellUpdaterInterface[]
*/
private $subscribedBellUpdaters = [];
public function subscribe(BellUpdaterInterface $bellUpdater): void
{
$this->subscribedBellUpdaters[] = $bellUpdater;
}
public function triggerUpdate(): void
{
foreach ($this->subscribedBellUpdaters as $bellUpdater) {
$bellUpdater->updateExpiredBells();
}
}
}
<?php
namespace Foodsharing\Modules\Bell;
/**
* Implement this interface if you want to subscribe to the BellUpdateTrigger.
*/
interface BellUpdaterInterface
{
/**
* This method gets called by the BellUpdateTrigger when a Bell is found that reached its expiration date and
* needs to be updated. Implement a function that gets all of your expired bells from the database and updates
* them.
*
* An update can be anything: A bell can also be removed when it got outdated.
*/
public function updateExpiredBells(): void;
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment