* 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
*/
class notifications_ajax {
class notifications_ajax
{
/**
* Appname
*/
@ -36,6 +37,11 @@ class notifications_ajax {
*/
const _type = 'base';
/**
* Do NOT consider notifications older than this
*/
const CUT_OFF_DATE = '-30days';
/**
* holds account object for user to notify
*
@ -64,22 +70,6 @@ class notifications_ajax {
*/
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
*
@ -98,15 +88,11 @@ class notifications_ajax {
* constructor
*
*/
public function __construct() {
public function __construct()
{
$this->response = Api\Json\Response::get();
$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->isPushServer = Api\Cache::getInstance('notifications', 'isPushServer', function ()
@ -147,18 +133,9 @@ class notifications_ajax {
*
* @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);
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]);
$this->update($notifymessages, null); // null = delete
}
/**
@ -172,80 +149,95 @@ class notifications_ajax {
* this status has been used more specifically for browser type
* of notifications.
*/
public function update_status($notifymessages, $status = "SEEN")
public function update_status(array $notifymessages, $status = "SEEN")
{
$notify_ids = $this->fetch_notify_ids($notifymessages);
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);
}
$this->update($notifymessages, $status);
}
/**
* gets all relevant notify ids based on given notify message data
* @param $notifymessages
* Update or delete the given notification messages, incl. not explicitly mentioned ones with same app:id
*
* @param array $notifymessages
* @param string|null $status use null to delete
* @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)
{
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'])) $notify_ids = array_unique(array_merge($notify_ids, $this->search_in_notify_data($data['data']['id'], $data['data']['app'])));
if (is_array($data['data'] ?? null) && !empty($data['data']['id']))
{
$app_ids[$data['data']['app']][$data['data']['id']] = $data['data']['id'];
}
$notify_ids[] = $data['id'];
}
else
{
array_push($notify_ids, (string)$data);
$notify_ids[] = $data;
}
}
return $notify_ids;
}
/**
* 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 {
// mariaDB supported query
$ret = $this->db->select(self::_notification_table, 'notify_id', array(
$cut_off = $this->db->quote(Api\DateTime::to(self::CUT_OFF_DATE, Api\DateTime::DATABASE));
try {
// MariaDB code using JSON_EXTRACT()
foreach($app_ids as $app => $ids)
{
$where = [
'account_id' => $this->recipient->account_id,
'notify_type' => self::_type,
'notify_data->"$.appname"' => $_appname,
'notify_data->"$.data.id"' => $_id
),
__LINE__,__FILE__,0 ,'ORDER BY notify_id DESC',self::_appname);
}
catch (Api\Db\Exception $e) {
// do it manual for all other DB
foreach($this->db->select(self::_notification_table, '*', array(
'account_id' => $this->recipient->account_id,
'notify_type' => self::_type
),
__LINE__,__FILE__,0 ,'ORDER BY notify_id DESC',self::_appname) as $row)
"JSON_EXTRACT(notify_data, '$.appname') = ".$this->db->quote($app),
"JSON_EXTRACT(notify_data, '$.data.id') IN (".implode(',', array_map([$this->db, 'quote'], array_unique($ids))).')',
'notify_created > '.$cut_off,
];
if (isset($status))
{
$data = json_decode($row['notify_data'], true);
if ($data['appname'] == $_appname && $data['data']['id'] == $_id) $ret[] = $row['notify_id'];
$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);
}
}
}
return $ret;
// other DBs
catch (Api\Db\Exception $e) {
foreach($this->db->select(self::_notification_table, 'notify_id,notify_data', [
'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,
'notify_type' => self::_type
];
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);
}
// 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);
}
/**
* 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));
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
* 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
*/
@ -120,13 +120,14 @@ class notifications_popup implements notifications_iface {
* @param array $_user_sessions
* @param array $_data
*/
private function save($_message, $_data) {
private function save($_message, $_data)
{
$result = $this->db->insert( self::_notification_table, array(
'account_id' => $this->recipient->account_id,
'notify_message' => $_message,
'notify_type' => self::_type,
'notify_data' => is_array($_data) ? json_encode($_data) : NULL,
'notify_created' => Api\DateTime::user2server('now'),
'notify_data' => $_data && is_array($_data) ? json_encode($_data) : NULL,
'notify_created' => new Api\DateTime(),
), false,__LINE__,__FILE__,self::_appname);
if ($result === false) throw new Exception("Can't save notification into SQL table");
$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 int $num_rows
* @return array
*/
public static function read($_account_id)
public static function read($_account_id, int $num_rows=100)
{
if (!$_account_id) return [];
$rs = $GLOBALS['egw']->db->select(self::_notification_table, '*', array(
'account_id' => $_account_id,
'notify_type' => self::_type
),
__LINE__,__FILE__,0 ,'ORDER BY notify_id DESC',self::_appname, 100);
// Fetch the total
$total = $GLOBALS['egw']->db->select(self::_notification_table, 'COUNT(*)', array(
'account_id' => $_account_id,
'notify_type' => self::_type
),
__LINE__,__FILE__,0 ,'',self::_appname)->fetchColumn();
$result = array();
if ($rs->NumRows() > 0) {
foreach ($rs as $notification) {
$actions = null;
$data = json_decode($notification['notify_data'], true);
if (!empty($data['appname']) && !empty($data['data']))
{
$_actions = Api\Hooks::process (array(
'location' => 'notifications_actions',
'data' => $data['data']
), $data['appname'], true);
$actions = $_actions[$data['appname']];
}
$result[] = array(
'id' => $notification['notify_id'],
'message' => $notification['notify_message'],
'status' => $notification['notify_status'],
'created' => Api\DateTime::server2user($notification['notify_created']),
'current' => new Api\DateTime('now'),
'actions' => is_array($actions)?$actions:NULL,
'extra_data' => $data['data'] ?? [],
);
/** @var Api\Db $db */
$db = $GLOBALS['egw']->db;
$result = [];
if (($total = $db->select(self::_notification_table, 'COUNT(*)', [
'account_id' => $_account_id,
'notify_type' => self::_type,
'notify_created > '.($cut_off=$db->quote(Api\DateTime::to(notifications_ajax::CUT_OFF_DATE, Api\DateTime::DATABASE))),
], __LINE__, __FILE__, false, '', self::_appname)->fetchColumn()))
{
$n = 0;
$chunk_size = 150;
do
{
$notification = null;
foreach ($rs=$db->select(self::_notification_table, '*', [
'account_id' => $_account_id,
'notify_type' => self::_type,
'notify_created > ' . $cut_off,
], __LINE__, __FILE__, $n, 'ORDER BY notify_id DESC', self::_appname, $chunk_size) as $notification)
{
$actions = null;
$data = json_decode($notification['notify_data'], true);
if (!empty($data['appname']) && !empty($data['data']))
{
$_actions = Api\Hooks::process(array(
'location' => 'notifications_actions',
'data' => $data['data']
), $data['appname'], true);
$actions = $_actions[$data['appname']];
}
$data = [
'id' => $notification['notify_id'],
'message' => $notification['notify_message'],
'status' => $notification['notify_status'],
'created' => Api\DateTime::server2user($notification['notify_created']),
'current' => new Api\DateTime('now'),
'actions' => is_array($actions) ? $actions : NULL,
'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;
}
else
{
$total--;
/* in case we want to show all
$result['id']['others'][] = $data;
*/
}
}
else
{
$result[] = $data;
}
}
$n += $chunk_size;
}
return ['rows' => $result, 'total'=> $total];
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)
*/
public static function deleteaccount($settings) {
public static function deleteaccount($settings)
{
$GLOBALS['egw']->db->delete( self::_notification_table, array(
'account_id' => $settings['account_id']
),__LINE__,__FILE__,self::_appname);