From 0a0636162589db34a9bac8eb1ef3f227c00c0d29 Mon Sep 17 00:00:00 2001 From: ralf Date: Mon, 25 Sep 2023 09:43:21 +0200 Subject: [PATCH] * Notifications: aggregate messages by app:id to return only latest one, also added a cut-off date of 30 days and cleaning older messages --- .../inc/class.notifications_ajax.inc.php | 190 +++++++----------- .../inc/class.notifications_popup.inc.php | 121 +++++++---- 2 files changed, 156 insertions(+), 155 deletions(-) diff --git a/notifications/inc/class.notifications_ajax.inc.php b/notifications/inc/class.notifications_ajax.inc.php index 507516abd1..758da29b0b 100644 --- a/notifications/inc/class.notifications_ajax.inc.php +++ b/notifications/inc/class.notifications_ajax.inc.php @@ -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; - } } \ No newline at end of file diff --git a/notifications/inc/class.notifications_popup.inc.php b/notifications/inc/class.notifications_popup.inc.php index 2c0f780416..3adccb5f5e 100644 --- a/notifications/inc/class.notifications_popup.inc.php +++ b/notifications/inc/class.notifications_popup.inc.php @@ -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,9 +307,10 @@ 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); } -} +} \ No newline at end of file