* Notifications: aggregate messages by app:id to return only latest one, also added a cut-off date of 30 days and cleaning older messages

This commit is contained in:
ralf 2023-09-25 09:43:21 +02:00
parent 20c93a0c7a
commit c5d3bb89ab
2 changed files with 156 additions and 155 deletions

View File

@ -15,7 +15,8 @@ use EGroupware\Api;
/** /**
* Ajax methods for notifications * Ajax methods for notifications
*/ */
class notifications_ajax { class notifications_ajax
{
/** /**
* Appname * Appname
*/ */
@ -36,6 +37,11 @@ class notifications_ajax {
*/ */
const _type = 'base'; const _type = 'base';
/**
* Do NOT consider notifications older than this
*/
const CUT_OFF_DATE = '-30days';
/** /**
* holds account object for user to notify * holds account object for user to notify
* *
@ -64,22 +70,6 @@ class notifications_ajax {
*/ */
private $db; private $db;
/**
* holds the users session data
*
* @var array
*/
var $session_data;
/**
* holds the users session data defaults
*
* @var array
*/
var $session_data_defaults = array(
'notified_mail_uids' => array(),
);
/** /**
* the xml response object * the xml response object
* *
@ -98,15 +88,11 @@ class notifications_ajax {
* constructor * constructor
* *
*/ */
public function __construct() { public function __construct()
{
$this->response = Api\Json\Response::get(); $this->response = Api\Json\Response::get();
$this->recipient = (object)$GLOBALS['egw']->accounts->read($GLOBALS['egw_info']['user']['account_id']); $this->recipient = (object)$GLOBALS['egw']->accounts->read($GLOBALS['egw_info']['user']['account_id']);
$this->config = (object)Api\Config::read(self::_appname);
$prefs = new Api\Preferences($this->recipient->account_id);
$this->preferences = $prefs->read();
$this->db = $GLOBALS['egw']->db; $this->db = $GLOBALS['egw']->db;
$this->isPushServer = Api\Cache::getInstance('notifications', 'isPushServer', function () $this->isPushServer = Api\Cache::getInstance('notifications', 'isPushServer', function ()
@ -147,18 +133,9 @@ class notifications_ajax {
* *
* @param array $notifymessages one or multiple notify_id(s) * @param array $notifymessages one or multiple notify_id(s)
*/ */
public function delete_message($notifymessages) public function delete_message(array $notifymessages)
{ {
$notify_ids = $this->fetch_notify_ids($notifymessages); $this->update($notifymessages, null); // null = delete
if (!empty($notify_ids))
{
$this->db->delete(self::_notification_table,array(
'notify_id' => $notify_ids,
'account_id' => $this->recipient->account_id,
'notify_type' => self::_type
),__LINE__,__FILE__,self::_appname);
}
$this->response->data(['deleted'=>$notify_ids]);
} }
/** /**
@ -172,80 +149,95 @@ class notifications_ajax {
* this status has been used more specifically for browser type * this status has been used more specifically for browser type
* of notifications. * of notifications.
*/ */
public function update_status($notifymessages, $status = "SEEN") public function update_status(array $notifymessages, $status = "SEEN")
{ {
$notify_ids = $this->fetch_notify_ids($notifymessages); $this->update($notifymessages, $status);
if (!empty($notify_ids))
{
$this->db->update(self::_notification_table,array('notify_status' => $status),array(
'notify_id' => $notify_ids,
'account_id' => $this->recipient->account_id,
'notify_type' => self::_type
),__LINE__,__FILE__,self::_appname);
}
} }
/** /**
* gets all relevant notify ids based on given notify message data * Update or delete the given notification messages, incl. not explicitly mentioned ones with same app:id
* @param $notifymessages *
* @param array $notifymessages
* @param string|null $status use null to delete
* @return array * @return array
*/ */
public function fetch_notify_ids ($notifymessages) protected function update(array $notifymessages, $status='SEEN')
{ {
$notify_ids = []; $notify_ids = $app_ids = [];
foreach ($notifymessages as $data) foreach ($notifymessages as $data)
{ {
if (is_array($data) && $data['id']) if (is_array($data) && !empty($data['id']))
{ {
array_push($notify_ids, (string)$data['id']); if (is_array($data['data'] ?? null) && !empty($data['data']['id']))
if (is_array($data['data'])) $notify_ids = array_unique(array_merge($notify_ids, $this->search_in_notify_data($data['data']['id'], $data['data']['app']))); {
$app_ids[$data['data']['app']][$data['data']['id']] = $data['data']['id'];
}
$notify_ids[] = $data['id'];
} }
else else
{ {
array_push($notify_ids, (string)$data); $notify_ids[] = $data;
} }
} }
return $notify_ids; $cut_off = $this->db->quote(Api\DateTime::to(self::CUT_OFF_DATE, Api\DateTime::DATABASE));
}
/**
* Fetches all notify_ids relevant to the entry
* @param $_id
* @param $_appname
* @return array
*/
public function search_in_notify_data($_id, $_appname)
{
$ret = [];
if ($_id && $_appname)
{
try { try {
// mariaDB supported query // MariaDB code using JSON_EXTRACT()
$ret = $this->db->select(self::_notification_table, 'notify_id', array( foreach($app_ids as $app => $ids)
{
$where = [
'account_id' => $this->recipient->account_id, 'account_id' => $this->recipient->account_id,
'notify_type' => self::_type, 'notify_type' => self::_type,
'notify_data->"$.appname"' => $_appname, "JSON_EXTRACT(notify_data, '$.appname') = ".$this->db->quote($app),
'notify_data->"$.data.id"' => $_id "JSON_EXTRACT(notify_data, '$.data.id') IN (".implode(',', array_map([$this->db, 'quote'], array_unique($ids))).')',
), 'notify_created > '.$cut_off,
__LINE__,__FILE__,0 ,'ORDER BY notify_id DESC',self::_appname); ];
if (isset($status))
{
$this->db->update(self::_notification_table, ['notify_status' => $status], $where, __LINE__, __FILE__, self::_appname);
} }
else
{
$this->db->delete(self::_notification_table, $where, __LINE__, __FILE__, self::_appname);
}
}
}
// other DBs
catch (Api\Db\Exception $e) { catch (Api\Db\Exception $e) {
// do it manual for all other DB foreach($this->db->select(self::_notification_table, 'notify_id,notify_data', [
foreach($this->db->select(self::_notification_table, '*', array( 'account_id' => $this->recipient->account_id,
'notify_type' => self::_type,
'notify_created > '.$cut_off,
"notify_data <> '[]'", // does not return NULL or '[]' rows
]) as $row)
{
if (($data = json_decode($row['notify_data'], true)) &&
isset($data['data']['id']) && in_array($data['data']['id'], $app_ids[$data['appname']] ?? []))
{
$notify_ids[] = $row['notify_id'];
}
}
}
$where = [
'notify_id' => array_unique($notify_ids),
'account_id' => $this->recipient->account_id, 'account_id' => $this->recipient->account_id,
'notify_type' => self::_type 'notify_type' => self::_type
), ];
__LINE__,__FILE__,0 ,'ORDER BY notify_id DESC',self::_appname) as $row) if (isset($status))
{ {
$data = json_decode($row['notify_data'], true); $this->db->update(self::_notification_table, ['notify_status' => $status], $where, __LINE__, __FILE__, self::_appname);
if ($data['appname'] == $_appname && $data['data']['id'] == $_id) $ret[] = $row['notify_id'];
} }
else
{
$this->db->delete(self::_notification_table, $where, __LINE__, __FILE__, self::_appname);
} }
// cleanup messages older than our cut-off-date
$this->db->delete(self::_notification_table, [
'notification_created <= '.$cut_off,
'notify_type' => self::_type
], __LINE__, __FILE__, self::_appname);
} }
return $ret;
}
/** /**
* gets all egwpopup notifications for calling user * gets all egwpopup notifications for calling user
* *
@ -257,30 +249,4 @@ class notifications_ajax {
$this->response->apply('app.notifications.append', array($entries['rows']??[], $browserNotify, $entries['total']??0)); $this->response->apply('app.notifications.append', array($entries['rows']??[], $browserNotify, $entries['total']??0));
return true; return true;
} }
/**
* restores the users session data for notifications
*
* @return boolean true
*/
private function restore_session_data() {
$session_data = Api\Cache::getSession(self::_appname, 'session_data');
if(is_array($session_data)) {
$this->session_data = $session_data;
} else {
$this->session_data = $this->session_data_defaults;
}
return true;
}
/**
* saves the users session data for notifications
*
* @return boolean true
*/
private function save_session_data() {
Api\Cache::setSession(self::_appname, 'session_data', $this->session_data);
return true;
}
} }

View File

@ -21,8 +21,8 @@ use EGroupware\Api;
* out the table to look if there is a notificaton for this * out the table to look if there is a notificaton for this
* client. The second stage is done in class.notifications_ajax.inc.php * client. The second stage is done in class.notifications_ajax.inc.php
*/ */
class notifications_popup implements notifications_iface { class notifications_popup implements notifications_iface
{
/** /**
* Appname * Appname
*/ */
@ -120,13 +120,14 @@ class notifications_popup implements notifications_iface {
* @param array $_user_sessions * @param array $_user_sessions
* @param array $_data * @param array $_data
*/ */
private function save($_message, $_data) { private function save($_message, $_data)
{
$result = $this->db->insert( self::_notification_table, array( $result = $this->db->insert( self::_notification_table, array(
'account_id' => $this->recipient->account_id, 'account_id' => $this->recipient->account_id,
'notify_message' => $_message, 'notify_message' => $_message,
'notify_type' => self::_type, 'notify_type' => self::_type,
'notify_data' => is_array($_data) ? json_encode($_data) : NULL, 'notify_data' => $_data && is_array($_data) ? json_encode($_data) : NULL,
'notify_created' => Api\DateTime::user2server('now'), 'notify_created' => new Api\DateTime(),
), false,__LINE__,__FILE__,self::_appname); ), false,__LINE__,__FILE__,self::_appname);
if ($result === false) throw new Exception("Can't save notification into SQL table"); if ($result === false) throw new Exception("Can't save notification into SQL table");
$push = new Api\Json\Push($this->recipient->account_id); $push = new Api\Json\Push($this->recipient->account_id);
@ -136,50 +137,83 @@ class notifications_popup implements notifications_iface {
/** /**
* read all notification messages for given recipient * Read the 100 most recent notification messages for given recipient
*
* We use a cut-off-date of 30day, not returning anything older!
*
* @param $_account_id * @param $_account_id
* @param int $num_rows
* @return array * @return array
*/ */
public static function read($_account_id) public static function read($_account_id, int $num_rows=100)
{ {
if (!$_account_id) return []; if (!$_account_id) return [];
$rs = $GLOBALS['egw']->db->select(self::_notification_table, '*', array( /** @var Api\Db $db */
$db = $GLOBALS['egw']->db;
$result = [];
if (($total = $db->select(self::_notification_table, 'COUNT(*)', [
'account_id' => $_account_id, 'account_id' => $_account_id,
'notify_type' => self::_type 'notify_type' => self::_type,
), 'notify_created > '.($cut_off=$db->quote(Api\DateTime::to(notifications_ajax::CUT_OFF_DATE, Api\DateTime::DATABASE))),
__LINE__,__FILE__,0 ,'ORDER BY notify_id DESC',self::_appname, 100); ], __LINE__, __FILE__, false, '', self::_appname)->fetchColumn()))
// Fetch the total {
$total = $GLOBALS['egw']->db->select(self::_notification_table, 'COUNT(*)', array( $n = 0;
$chunk_size = 150;
do
{
$notification = null;
foreach ($rs=$db->select(self::_notification_table, '*', [
'account_id' => $_account_id, 'account_id' => $_account_id,
'notify_type' => self::_type 'notify_type' => self::_type,
), 'notify_created > ' . $cut_off,
__LINE__,__FILE__,0 ,'',self::_appname)->fetchColumn(); ], __LINE__, __FILE__, $n, 'ORDER BY notify_id DESC', self::_appname, $chunk_size) as $notification)
$result = array(); {
if ($rs->NumRows() > 0) {
foreach ($rs as $notification) {
$actions = null; $actions = null;
$data = json_decode($notification['notify_data'], true); $data = json_decode($notification['notify_data'], true);
if (!empty($data['appname']) && !empty($data['data'])) if (!empty($data['appname']) && !empty($data['data']))
{ {
$_actions = Api\Hooks::process (array( $_actions = Api\Hooks::process(array(
'location' => 'notifications_actions', 'location' => 'notifications_actions',
'data' => $data['data'] 'data' => $data['data']
), $data['appname'], true); ), $data['appname'], true);
$actions = $_actions[$data['appname']]; $actions = $_actions[$data['appname']];
} }
$result[] = array( $data = [
'id' => $notification['notify_id'], 'id' => $notification['notify_id'],
'message' => $notification['notify_message'], 'message' => $notification['notify_message'],
'status' => $notification['notify_status'], 'status' => $notification['notify_status'],
'created' => Api\DateTime::server2user($notification['notify_created']), 'created' => Api\DateTime::server2user($notification['notify_created']),
'current' => new Api\DateTime('now'), 'current' => new Api\DateTime('now'),
'actions' => is_array($actions)?$actions:NULL, 'actions' => is_array($actions) ? $actions : NULL,
'extra_data' => $data['data'] ?? [], 'extra_data' => $data['data'] ?? [],
); ];
// aggregate by app:id reporting only the newest entry
if (!empty($data['extra_data']['id']))
{
if (!isset($result[$id = $data['extra_data']['app'] . ':' . $data['extra_data']['id']]))
{
$result[$id] = $data;
} }
return ['rows' => $result, 'total'=> $total]; else
{
$total--;
/* in case we want to show all
$result['id']['others'][] = $data;
*/
}
}
else
{
$result[] = $data;
}
}
$n += $chunk_size;
}
while(!$notification || count($result) < min($num_rows, $total));
return ['rows' => array_values($result), 'total'=> $total];
} }
} }
@ -273,7 +307,8 @@ class notifications_popup implements notifications_iface {
* *
* @param settings array with keys account_id and new_owner (new_owner is optional) * @param settings array with keys account_id and new_owner (new_owner is optional)
*/ */
public static function deleteaccount($settings) { public static function deleteaccount($settings)
{
$GLOBALS['egw']->db->delete( self::_notification_table, array( $GLOBALS['egw']->db->delete( self::_notification_table, array(
'account_id' => $settings['account_id'] 'account_id' => $settings['account_id']
),__LINE__,__FILE__,self::_appname); ),__LINE__,__FILE__,self::_appname);