route = ResponseRoutes::getInstance(); $this->defineTables(); $this->karma = KarmaManager::for('responses', $this->responses->getFullTableName()); $this->responses->setKarma($this->karma->karmaTable()); add_action('deleted_user', [$this, 'handleUserDeletion'], 10, 1); } protected function defineTables():void { $table = CustomTable::for('responses'); $table->setColumns([ 'id' => 'bigint(20) unsigned NOT NULL AUTO_INCREMENT', 'item_id' => "{$table->getPostIDType()} NOT NULL", 'type' => 'varchar(50) NOT NULL', 'user_id' => "{$table->getUserIdType()} NOT NULL", 'parent_id' => 'bigint(20) unsigned DEFAULT 0', 'response' => 'text NOT NULL', 'upvotes' => 'int NOT NULL DEFAULT 0', 'downvotes' => 'int NOT NULL DEFAULT 0', 'karmic_score' => 'int NOT NULL DEFAULT 0', 'history' => 'json DEFAULT {}', 'status' => "ENUM('publish', 'hidden','flagged','deleted') DEFAULT 'publish'", 'is_user_deleted'=> 'tinyint(1) DEFAULT 0', 'created_at' => 'datetime DEFAULT CURRENT_TIMESTAMP', 'updated_at' => 'datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP', ]); $table->setKeys([ ['key' => 'PRIMARY', 'value' => '`id`'], '`item_lookup` (`item_id`)', '`user_lookup` (`user_id`)', '`parent_lookup` (`parent_id`)', '`karma` (`karmic_score`)', '`upvote` (`upvotes`)', '`downvote` (`downvotes`)', ]); $base = BASE; $table->setConstraints([ "CONSTRAINT `{$base}responses_item` FOREIGN KEY (`item_id`) REFERENCES `{$table->getPostTable()}` (`ID`) ON DELETE CASCADE", ]); $table->defineTable(); $this->responses = $table; } public function responseExists(int $id):bool { return (bool) $this->responses->get([ 'id' => $id, ]); } public function updateKarma(int $id, int $upvotes, int $downvotes, int $score):bool { return $this->responses->update([ 'upvotes' => $upvotes, 'downvotes' => $downvotes, 'karmic_score' => $score, ], [ 'id' => $id ]); } protected function itemExists(int $item_id, string $type):bool { return (bool)$this->getOriginalItem($item_id, $type); } public function getOriginalItem(int $item_id, string $type):object|false { [$method, $type] = $this->determineType($type); if (!$method) { return false; } $item = match($method) { 'post' => get_post($item_id), 'term' => get_term($item_id, $type), 'user' => get_userdata($item_id), 'response' => $this->responses->get(['id' => $item_id]), default => false }; return (is_wp_error($item)) ? false : $item; } public function getOriginalItemAuthors(int $item_id, string $type):array { [$method, $type] = $this->determineType($type); $item = $this->getOriginalItem($item_id, $type); return match ($method) { 'post' => [$item->post_author], 'term' => jvbGetTermOwners($item_id), 'user' => [$item->ID], 'response' => [$item->user_id], }; } protected function determineType(string $type):array { $method = match ($type) { 'post', 'page', 'product' => 'post', 'post_tag', 'category', 'term', 'taxonomy' => 'term', 'user' => 'user', 'response' => 'response', default => false }; if (!$method) { $registrar = Registrar::getInstance($type); $type = jvbCheckBase($type); if ($registrar) { $method = $registrar->getType(); } } if (!$method) { error_log('[Responses]itemExists Invalid type: '.$type); return [false, false]; } return [$method, $type]; } public function respondTo(int $item_id, string $type, int $user_id, string $response, ?int $parent_id = null):array { if (!$this->itemExists($item_id, $type)) { return [ 'success' => false, 'message' => 'Item does not exist', ]; } if (empty($response)) { return [ 'success' => false, 'message' => 'Response content is empty' ]; } $registrar = Registrar::getInstance($type); if ($registrar) { $type = $registrar->getBased(); } $created = $this->responses->insert([ 'item_id' => $item_id, 'user_id' => $user_id, 'parent_id' => $parent_id, 'response' => $response, 'type' => $type ]); $users = $this->getOriginalItemAuthors($item_id, $type); if ($parent_id) { $users = array_merge($users, $this->getNotifyableUsers($parent_id)); } $users = array_unique($users); NotificationManager::getInstance()->unnotify( $users, 'new_response', $user_id, ); return [ 'success' => (bool)$created, 'message' => $created ? 'Response posted successfully' : 'Response not posted' ]; } public function editResponse(int $id, string $response):array { $original = $this->responses->get(['id' => $id]); if (!$original) { return [ 'success' => false, 'message' => 'Original response not found' ]; } if (get_current_user_id() !== $original->user_id) { return [ 'success' => false, 'message' => 'This is not your response to edit' ]; } $history = $this->pushHistory($original); $updated = $this->responses->update([ 'history' => $history, 'response' => sanitize_text_field($response) ], [ 'user_id' => $original->user_id, 'id' => $id ]); return [ 'success' => (bool)$updated, 'message' => $updated ? 'Response edited successfully' : 'Response not edited' ]; } public function deleteResponse(int $id):array { $original = $this->responses->get(['id' => $id]); if (!$original) { return [ 'success' => true, 'message' => 'Item does not exist', ]; } if ($original->user_id !== get_current_user_id()) { return [ 'success' => false, 'message' => 'This is not your response to delete' ]; } $deleted = $this->responses->update([ 'status' => 'deleted', 'history' => $this->pushHistory($original), 'response' => '[ deleted response ]' ], [ 'id' => $id, ]); NotificationManager::getInstance()->unnotify( $this->getNotifyableUsers($id), 'new_response', $original->user_id, ); return [ 'success' => (bool)$deleted, 'message' => $deleted ? 'Response deleted successfully' : 'Response not deleted' ]; } public function hideResponse(int $id):array { $original = $this->responses->get(['id' => $id]); if (!$original) { return [ 'success' => false, 'message' => 'Item does not exist', ]; } $currentUser = get_current_user_id(); $isCommentOwner = $original->user_id === $currentUser; if ($isCommentOwner) { return $this->deleteResponse($id); } $item = $this->getOriginalItem($original->item_id, $original->type); $authorIDs = $this->getOriginalItemAuthors($original->item_id, $original->type); $isAuthor = in_array($currentUser, $authorIDs); if (!$isAuthor) { return [ 'success' => false, 'message' => 'You cannot hide this response.' ]; } $updated = $this->responses->update([ 'status' => 'hidden', ], [ 'id' => $id, ]); return [ 'success' => (bool)$updated, 'message' => $updated ? 'Response hidden successfully' : 'Response not hidden' ]; } public function getUserResponses(int $userID, $args = [], bool $includeChildren = true):array { $args = wp_parse_args($args, [ 'orderby' => 'created_at', 'order' => 'DESC', 'page' => 1, 'per_page' => 20, ]); $where = [ 'user_id' => $userID, 'parent_id' => null ]; if (array_key_exists('item_id', $args)) { $where['item_id'] = $args['item_id']; } $responses = $this->responses->getMany(array_merge([ 'where' => $where, ], $args), false, ARRAY_A); if ($includeChildren) { $responses['items'] = array_map(function ($r) use ($args) { $r['children'] = $this->getChildResponses($r['id'], $args); return $r; }, $responses['items']); } return ['items'=> $responses]; } public function getChildResponses(int $parentID, array $args, bool $includeChildren = true):array { $args = wp_parse_args($args, [ 'orderby' => 'created_at', 'order' => 'DESC', 'page' => 1, 'per_page' => 20, ]); $responses = $this->responses->getMany(array_merge([ 'where' => [ 'id' => $parentID, ] ], $args), true, ARRAY_A); if ($includeChildren) { $responses = array_map(function ($r) use ($args) { $r['children'] = $this->getChildResponses($r['id'], $args); return $r; }, $responses); } return $responses; } public function getAllItemResponses(int $itemID, array $oldArgs = [], bool $includeChildren = true):array { $args = wp_parse_args($oldArgs, [ 'orderby' => 'created_at', 'order' => 'DESC', 'page' => 1, 'per_page' => 20, ]); if (array_key_exists('type', $oldArgs) || array_key_exists('content', $oldArgs) || array_key_exists('post_type', $oldArgs)) { $args['type'] = jvbCheckBase($oldArgs['type']??$oldArgs['content']??$oldArgs['post_type']); } $where = [ 'item_id' => $itemID, 'parent_id' => null ]; $responses = $this->responses->getMany(array_merge([ 'where' => $where, ], $args), false, ARRAY_A); if ($includeChildren) { $responses['items'] = array_map(function ($r) use ($args) { $r['children'] = $this->getChildResponses($r['id'], $args); return $r; }, $responses['items']); } return ['items'=> $responses]; } public function handleUserDeletion(int $user_id):void { $updated = $this->responses->update([ 'is_user_deleted' => 1, ], [ 'user_id' => $user_id, ]); Errors::getInstance()->log( 'Responses', 'Anonymized responses for deleted user', [ 'user_id' => $user_id, 'count' => $updated, ], 'info' ); } protected function getNotifyableUsers(int $id):array { $original = $this->responses->get(['id' => $id]); if (!$original) { return []; } $users = [$original->user_id]; if ($original->parent_id) { $users = array_merge($users, $this->getNotifyableUsers($original->parent_id)); } $users = array_merge($users, $this->getOriginalItemAuthors($original->item_id, $original->type)); return array_unique($users); } protected function pushHistory(object $original):array { $history = json_decode($original->history, true); $history[] = [ 'edited_at' => $original->updated_at, 'response' => $original->response, 'upvotes' => $original->upvotes, 'downvotes' => $original->downvotes, ]; return $history; } }