defineTypes(); $this->defineTable(); } private function defineTypes():void { $types = ['system_message' => [ 'icon' => 'info', 'digest'=> true ]]; if (Features::forSite()->has('favourites')) { $types = array_merge($types, [ 'new_favourite' => [ 'icon' => 'heart', 'digest'=> true ], 'list_shared' => [ 'icon' => 'list-heart', 'digest'=> false ], 'list_share_status' => [ 'icon' => 'list-heart', 'digest'=> false ] ]); } $contentTax = Registrar::getFeatured('is_content', 'term'); $verifyEntry = Registrar::getFeatured('verify_entry', 'term'); if (!empty(array_intersect($contentTax, $verifyEntry))) { $types = array_merge($types, [ 'entry_requested' => [ 'icon' => 'user-circle-plus', 'digest'=> true, 'action'=> ['approve', 'reject', 'dismiss'] ], 'entry_approved' => [ 'icon' => 'user-circle-check', 'digest'=> false ], 'entry_denied' => [ 'icon' => 'prohibit', 'digest'=> false ], 'entry_revoked' => [ 'icon' => 'warning-circle', 'digest'=> false ] ]); } $invitable = Registrar::getFeatured('invitable'); if (!empty($invitable)) { $types = array_merge($types, [ 'invitation_requested' => [ 'icon' => 'plus-circle', 'digest'=> true, 'note' => true, 'action'=> ['approve','reject','dismiss'] ], 'invitation_granted' => [ 'icon' => 'check-circle', 'digest'=> false ], 'invitation_revoked' => [ 'icon' => 'x-circle', 'digest'=> false ], 'invitation_accepted' => [ 'icon' => 'check-circle', 'digest'=> false ], 'invitation_refused' => [ 'icon' => 'x-circle', 'digest'=> false ] ]); } $approvals = Registrar::getFeatured('approve_new'); if (!empty($approvals)) { $tmp = ['user', 'term', 'post']; $app = []; foreach ($tmp as $t) { $approvals = Registrar::getFeatured('approve_new', $t); if (!empty($approvals)) { $app = array_merge($app, [ $t.'_new' => [ 'icon' => 'plus-circle', 'digest'=> true ], //For newly created items $t.'_approved' => [ 'icon' => 'check-circle', 'digest'=> false ], //For notices of acceptance $t.'_denied' => [ 'icon' => 'x-circle', 'digest'=> false ], //For notices of denial $t.'_status' => [ 'icon' => 'info', 'digest'=> true ], //For final status ]); } } $types = array_merge($types, $app); } $this->types = $types; } private function defineTable():void { $table = CustomTable::for('notifications_main'); $typeEnum = implode(',',array_map(function($type) { return '`'.$type.'`';}, array_keys($this->types))); $table->setColumns([ 'id' => 'bigint(20) unsigned NOT NULL AUTO_INCREMENT', 'for_user' => $table->getUserIDType().' NOT NULL', 'from_user' => $table->getUserIDType().' NOT NULL', 'target_id' => 'bigint(20) DEFAULT NULL', 'target_type' => 'varchar(30) DEFAULT NULL', 'type' => "ENUM({$typeEnum}) NOT NULL DEFAULT 'system_message'", 'status' => "ENUM('unread', 'read', 'actioned', 'dismissed') NOT NULL DEFAULT 'unread'", 'priority' => "ENUM('low','normal','high') NOT NULL DEFAULT 'normal'", 'message' => 'varchar(255) DEFAULT NULL', 'action_taken' => 'tinyint(1) NOT NULL DEFAULT 0', 'result' => 'JSON DEFAULT NULL', 'created_at' => 'datetime NOT NULL DEFAULT CURRENT_TIMESTAMP', 'read_at' => 'datetime DEFAULT NULL', 'actioned_at' => 'datetime DEFAULT NULL', 'updated_at' => 'datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP' ]); $table->setKeys([ ['key' => 'PRIMARY', 'value' => '(`id`)'], '`user_status` (`for_user`, `status`)', '`target_lookup` (`target_id`, `target_type`)', '`unread_notifications` (`for_user`, `status`, `created_at`)', '`requires_action` (`for_user`, `requires_action`, `action_taken`)', '`from_user_lookup` (`for_user`, `from_user`, `target_id`, `target_type`, `type`)' ]); $base = BASE; $table->setConstraints([ "CONSTRAINT `{$base}notify_owner` FOREIGN KEY (`for_user`) REFERENCES `{$table->getUserTable()}` (`ID`) ON DELETE CASCADE", "CONSTRAINT `{$base}from_user` FOREIGN KEY (`from_user`) REFERENCES `{$table->getUserTable()}` (`ID`) ON DELETE CASCADE" ]); $table->defineTable(); $this->notifications = $table; } protected function checkArgs(array $args):array { if (empty($args)) { return $args; } $allowedKeys = ['target_id', 'target_type','priority','message']; $allowed = array_filter($args, function($key) use ($allowedKeys) { return !in_array($key, $allowedKeys); }, ARRAY_FILTER_USE_KEY); $notAllowed = array_diff($allowedKeys, $allowed); if (!empty($notAllowed)) { error_log('[Notifications]::checkArgs Attempted keys not allowed:'.print_r($notAllowed, true)); } $out = []; foreach ($allowed as $key => $value) { switch ($key) { case 'target_id': $value = absint($value); if ($value > 0) { $out[$key] = $value; } break; case 'target_type': $registrar = Registrar::getInstance($value); if ($registrar) { $out[$key] = $value; } break; case 'priority': $value = strtolower($value); $value = in_array($value, ['low','normal','high']) ? $value : false; if ($value){ $out[$key] = $value; } break; case 'message': $value = sanitize_text_field($value); $out[$key] = $value; break; } } return $out; } public function notify(int|array $user_ids, string $n_type, int $fromUser = 0, array $args = []):bool { $args = $this->checkArgs($args); $n_type = strtolower($n_type); if (!in_array($n_type, array_keys($this->types))) { error_log('[Notifications]::notify Invalid notification type: '.$n_type); return false; } if (is_array($user_ids)) { return $this->notifications->transaction( function() use ($user_ids, $n_type, $fromUser, $args) { $results = []; foreach ($user_ids as $user_id) { $results[$user_id] = $this->notifications->findOrCreate([ 'for_user' => $user_id, 'from_user' => $fromUser, 'target_id' => $args['target_id']??null, 'target_type'=> $args['target_type']??null, 'type' => $n_type ], [ 'message' => $args['message']??null, ]); } return true; } ); } return $this->notifications->findOrCreate([ 'for_user' => $user_ids, 'from_user' => $fromUser, 'target_id' => $args['target_id']??null, 'target_type'=> $args['target_type']??null, 'type' => $n_type ], [ 'message' => $args['message']??null, ]); } public function unnotify(int|array $user_ids, string $n_type, int $fromUser = 0, array $args = []):bool { $args = $this->checkArgs($args); $n_type = strtolower($n_type); if (!in_array($n_type, array_keys($this->types))) { error_log('[Notifications]::notify Invalid notification type: '.$n_type); return false; } if (is_array($user_ids)) { return $this->notifications->transaction( function() use ($user_ids, $n_type, $fromUser, $args) { foreach ($user_ids as $user_id) { $item = $this->notifications->get([ 'for_user' => $user_id, 'from_user' => $fromUser, 'target_id' => $args['target_id']??null, 'target_type'=> $args['target_type']??null, 'type' => $n_type ]); if ($item) { $this->notifications->delete([ 'for_user' => $user_id, 'from_user' => $fromUser, 'target_id' => $args['target_id']??null, 'target_type'=> $args['target_type']??null, 'type' => $n_type ]); } } return true; } ); } $item = $this->notifications->get([ 'for_user' => $user_ids, 'from_user' => $fromUser, 'target_id' => $args['target_id']??null, 'target_type'=> $args['target_type']??null, 'type' => $n_type ]); if ($item) { return $this->notifications->delete([ 'for_user' => $user_ids, 'from_user' => $fromUser, 'target_id' => $args['target_id']??null, 'target_type'=> $args['target_type']??null, 'type' => $n_type ]); } return true; } public function getUserNotifications(int $user_id, array $where = []):array { $gotWhere = $where; $allowedKeys = ['from_user','target_id', 'target_type','type','status','priority','action_taken','created_at','read_at','actioned_at','updated_at']; $where = array_filter($gotWhere, function($key) use ($allowedKeys) { return in_array($key, $allowedKeys); }, ARRAY_FILTER_USE_KEY); $notAllowed = array_diff($gotWhere, $allowedKeys); if (!empty($notAllowed)){ error_log('[Notifications]::getUserNotifications Invalid where arguments, removed from request: '.print_r($notAllowed, true)); } return $this->notifications->getMany(array_merge([ 'for_user' => $user_id, 'status' => 'unread', //default to unread, but the merge will override if different ], $where)); } public function markRead(int $userID, int|array $notification_id):bool { if (is_int($notification_id)) { $notification = $this->notifications->get(['id' => $notification_id]); if (!$notification) { return false; } if ($notification['for_user'] !== $userID) { return false; } return (bool) $this->notifications->update([ 'status' => 'read' ], [ 'id' => $notification_id ]); } try { $this->notifications->transaction( function () use ($notification_id, $userID) { foreach ($notification_id as $id) { $notification = $this->notifications->get(['id' => $notification_id]); if (!$notification) { continue; } if ($notification['for_user'] !== $userID) { continue; } $this->notifications->update([ 'status' => 'read' ], [ 'id' => $id ]); } } ); return true; } catch (Exception $e) { return false; } } public function markDismissed(int $userID, int|array $notification_id):bool { if (is_int($notification_id)) { $notification = $this->notifications->get(['id' => $notification_id]); if (!$notification) { return false; } if ($notification['for_user'] !== $userID) { return false; } return (bool) $this->notifications->update([ 'status' => 'dismissed' ], [ 'id' => $notification_id ]); } try { $this->notifications->transaction( function () use ($notification_id, $userID) { foreach ($notification_id as $id) { $notification = $this->notifications->get(['id' => $notification_id]); if (!$notification) { continue; } if ($notification['for_user'] !== $userID) { continue; } $this->notifications->update([ 'status' => 'dismissed' ], [ 'id' => $id ]); } } ); return true; } catch (Exception $e) { return false; } } public function markActioned(int $userID, int|array $notification_id, array $result = []):bool { $update = [ 'status' => 'actioned', 'action_taken'=> 1, ]; if (!empty($result)) { $update['result'] = json_encode($result); } if (is_int($notification_id)) { $notification = $this->notifications->get(['id' => $notification_id]); if (!$notification) { return false; } if ($notification['for_user'] !== $userID) { return false; } return (bool) $this->notifications->update( $update, [ 'id' => $notification_id ] ); } try { $this->notifications->transaction( function () use ($notification_id, $userID, $update) { foreach ($notification_id as $id) { $notification = $this->notifications->get(['id' => $notification_id]); if (!$notification) { continue; } if ($notification['for_user'] !== $userID) { continue; } $this->notifications->update( $update, [ 'id' => $notification_id ] ); } } ); return true; } catch (Exception $e) { return false; } } }