<?php
|
namespace JVBase\managers\Notifications;
|
|
use Exception;
|
use JVBase\managers\CustomTable;
|
use JVBase\registrar\Registrar;
|
use JVBase\base\Site;
|
|
if (!defined('ABSPATH')) {
|
exit; // Exit if accessed directly
|
}
|
/**
|
* Preferences
|
* Manages preferences for a user's favourites
|
**/
|
class Notifications
|
{
|
protected array $types;
|
protected CustomTable $notifications;
|
|
public function __construct()
|
{
|
$this->defineTypes();
|
$this->defineTable();
|
}
|
|
private function defineTypes():void
|
{
|
$types = ['system_message' => [
|
'icon' => 'info',
|
'digest'=> true
|
]];
|
if (Site::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;
|
}
|
|
public function getNotificationTypes(bool $all = false):array
|
{
|
return ($all) ? $this->types : array_keys($this->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`, `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;
|
}
|
}
|
|
|
}
|