Jake Vanderwerf
2026-08-28 a9654f00c8fbc9934289d761843ddd071025013f
inc/rest/routes/ResponseRoutes.php
@@ -1,7 +1,9 @@
<?php
namespace JVBase\rest\routes;
use JVBase\JVB;
use JVBase\Instance;
use JVBase\managers\Cache;
use JVBase\managers\Responses;
use JVBase\rest\Rest;
use JVBase\managers\CustomTable;
use WP_REST_Request;
@@ -19,6 +21,7 @@
 */
class ResponseRoutes extends Rest
{
   use Instance;
   protected int $perPage = 20;
   protected CustomTable $table;
   protected CustomTable $karmaTable;
@@ -28,12 +31,8 @@
      $this->cacheName = 'responses';
      $this->cacheTtl = 1800; // 30 minutes
      parent::__construct();
      $this->cache = Cache::for($this->cacheName, 'response', $this->cacheTtl);
      $this->table = CustomTable::for('responses');
      $this->karmaTable = CustomTable::for('karma_response');
      add_filter(BASE.'handle_bulk_operation', [$this, 'processOperation'], 10, 3);
      add_action('deleted_user', [$this, 'handleUserDeletion'], 10, 1);
   }
   public function registerRoutes(): void
@@ -57,38 +56,15 @@
    */
   public function getResponses(WP_REST_Request $request): WP_REST_Response
   {
      $item_id = (int) $request->get_param('item_id');
      if (!$item_id) {
      $params = $request->get_params();
      if (!array_key_exists('item_id', $params)) {
         return $this->error('Missing item ID', 'missing_item_id');
      }
      $item_id = absint($params['item_id']);
      // Build query args
      $args = $this->buildQueryArgs($request);
      $cacheKey = $this->cache->generateKey(array_merge(['item_id' => $item_id], $args));
      // Check headers for 304 Not Modified
      $headerCheck = $this->checkHeaders($request, $cacheKey);
      if ($headerCheck) {
         return $headerCheck;
      }
      // Check cache
      $cached = $this->cache->get($cacheKey);
      if ($cached) {
         return $this->success($cached);
      }
      // Get responses
      $responses = $this->getItemResponse($item_id, $args);
      if (is_wp_error($responses)) {
         return $this->notFound($responses->get_error_message());
      }
      // Cache and return
      $this->cache->set($cacheKey, $responses);
      return $this->success($responses);
      return $this->success(Responses::getInstance()->getAllItemResponses($item_id, $args));
   }
   /**
@@ -103,7 +79,6 @@
         return $this->unauthorized();
      }
      $operation_id = $data['id'] ?? uniqid('response_');
      $action = sanitize_text_field($data['action'] ?? '');
      if (!in_array($action, ['create', 'delete', 'update'])) {
@@ -116,284 +91,38 @@
            return $this->error('Missing required fields', 'missing_fields');
         }
      }
      // Prepare data for queue
      $queue_data = match($action) {
         'create' => [
            'item_id' => (int) $data['item_id'],
            'parent_id' => isset($data['parent_id']) ? (int) $data['parent_id'] : null,
            'response' => wp_kses_post($data['response']),
            'content' => sanitize_text_field($data['content'])
         ],
         'update' => [
            'response_id' => (int) $data['response_id'],
            'response' => isset($data['response']) ? wp_kses_post($data['response']) : null,
            'status' => isset($data['status']) && in_array($data['status'], ['published', 'hidden', 'flagged'])
               ? $data['status']
               : null
         ],
         'delete' => [
            'response_id' => (int) $data['response_id']
         ]
      $responses = Responses::getInstance();
      $response = match($action) {
         'create' => $responses->respondTo(
            absint($data['item_id']),
            sanitize_text_field($data['content']),
            $user_id,
            wp_kses_post($data['response']),
            isset($data['parent_id']) ? absint($data['parent_id']) : null
         ),
         'update' => $responses->editResponse(
            absint($data['response_id']),
            wp_kses_post($data['response']??'')
         ),
         'delete' => $responses->deleteResponse(absint($data['response_id']))
      };
      // Queue the operation
      JVB()->queue()->queueOperation(
         $action . '_response',
         $user_id,
         $queue_data,
         [
            'operation_id' => 'u' . $user_id . '_' . $operation_id,
            'priority' => 'high'
         ]
      );
      return $this->queued($operation_id);
      if ($response['success']) {
         return $this->success($response);
      }
      return $this->error($response['message']);
   }
   /**
    * Get responses with karma calculations
    */
   protected function getItemResponse(int $item_id, array $args = []): array|WP_Error
   public function getItemResponse(int $item_id, array $args = []): array|WP_Error
   {
      $defaults = [
         'page' => 1,
         'per_page' => $this->perPage,
         'orderby' => 'created_at',
         'order' => 'DESC',
         'parent_id' => null
      ];
      $args = wp_parse_args($args, $defaults);
      // Verify post exists
      $post = get_post($item_id);
      if (!$post) {
         return new WP_Error('not_found', 'Item not found');
      }
      // Build query with karma calculations
      $query = "
            SELECT r.*,
                COALESCE((SELECT COUNT(*) FROM {$this->table->getFullTableName()} WHERE parent_id = r.id), 0) as reply_count,
                COALESCE((SELECT COUNT(*) FROM {$this->karmaTable->getFullTableName()} WHERE item_id = r.id AND vote = 'up'), 0) as upvotes,
                COALESCE((SELECT COUNT(*) FROM {$this->karmaTable->getFullTableName()} WHERE item_id = r.id AND vote = 'down'), 0) as downvotes
            FROM {$this->table->getFullTableName()} r
            WHERE r.item_id = %d
        ";
      $query_args = [$item_id];
      // Filter by parent_id
      if (isset($args['parent_id'])) {
         $query .= " AND r.parent_id " . ($args['parent_id'] === null ? "IS NULL" : "= %d");
         if ($args['parent_id'] !== null) {
            $query_args[] = $args['parent_id'];
         }
      }
      // Get total count for pagination
      $count_query = str_replace(
         ['SELECT r.*,', 'COALESCE((SELECT COUNT(*) FROM', 'as reply_count,', 'as upvotes,', 'as downvotes'],
         ['SELECT COUNT(*)', '', '', '', ''],
         $query
      );
      $count_query = preg_replace('/COALESCE\([^)]+\)[^,]*,?/', '', $count_query);
      $total_items = (int) $this->table->queryVar($count_query, $query_args);
      // Add ordering
      $order_column = in_array($args['orderby'], ['created_at', 'updated_at'])
         ? "r.{$args['orderby']}"
         : $args['orderby'];
      $query .= " ORDER BY {$order_column} {$args['order']}";
      // Add pagination
      $query .= " LIMIT %d OFFSET %d";
      $query_args[] = $args['per_page'];
      $query_args[] = ($args['page'] - 1) * $args['per_page'];
      // Get responses
      $responses = $this->table->queryResults($query, $query_args);
      $responses = Responses::getInstance()->getAllItemResponses($item_id, $args);
      // Format responses
      $items = array_map([$this, 'formatItem'], $responses);
      $responses['items'] = array_map([$this, 'formatItem'], $responses);
      return [
         'items' => $items,
         'has_more' => $args['page'] < ceil($total_items / $args['per_page']),
         'total_items' => $total_items,
         'total_pages' => (int) ceil($total_items / $args['per_page'])
      ];
   }
   /**
    * Process queue operations
    */
   public function processOperation(WP_Error|array $result, object $operation, array $data): WP_Error|array
   {
      if (!in_array($operation->type, ['create_response', 'update_response', 'delete_response'])) {
         return $result;
      }
      return match($operation->type) {
         'create_response' => $this->createResponse($operation, $data),
         'update_response' => $this->updateResponse($data),
         'delete_response' => $this->deleteResponse($data),
         default => $result
      };
   }
   /**
    * Create a new response
    */
   protected function createResponse(object $operation, array $data): array
   {
      $response_id = $this->table->insert([
         'item_id' => $data['item_id'],
         'content' => $data['content'],
         'user_id' => $operation->user_id,
         'parent_id' => $data['parent_id'],
         'response' => $data['response'],
         'status' => 'published'
      ]);
      if (!$response_id) {
         $this->logError('Failed to insert response', [
            'data' => $data,
            'error' => $this->table->getLastError()
         ]);
         return ['success' => false, 'result' => 'Failed to create response'];
      }
      // Send notifications
      $this->sendNotifications($data['item_id'], $data['parent_id'], $response_id, $operation->user_id);
      // Clear cache
      $this->cache->flush();
      return ['success' => true, 'result' => $response_id];
   }
   /**
    * Update a response
    */
   protected function updateResponse(array $data): array
   {
      $update_data = [];
      if (isset($data['response'])) {
         $update_data['response'] = $data['response'];
         $update_data['updated_at'] = current_time('mysql');
      }
      if (isset($data['status'])) {
         $update_data['status'] = $data['status'];
      }
      if (empty($update_data)) {
         return ['success' => false, 'result' => 'No fields to update'];
      }
      $updated = $this->table->update(
         $update_data,
         ['id' => $data['response_id']]
      );
      if ($updated === false) {
         $this->logError('Failed to update response', [
            'data' => $data,
            'error' => $this->table->getLastError()
         ]);
         return ['success' => false, 'result' => 'Failed to update response'];
      }
      $this->cache->flush();
      return ['success' => true, 'result' => $updated];
   }
   /**
    * Delete a response (or mark as deleted if it has replies)
    */
   protected function deleteResponse(array $data): array
   {
      $response = $this->table->get(['id' => $data['response_id']]);
      if (!$response) {
         return ['success' => false, 'result' => 'Response not found'];
      }
      // Check if it has replies
      $has_replies = $this->table->where(['parent_id' => $data['response_id']])->countResults() > 0;
      if ($has_replies) {
         // Don't delete, just mark as deleted
         $updated = $this->table->update(
            [
               'response' => '[ deleted ]',
               'status' => 'deleted',
               'updated_at' => current_time('mysql')
            ],
            ['id' => $data['response_id']]
         );
         $this->cache->flush();
         return ['success' => true, 'result' => $updated];
      }
      // No replies, safe to delete
      $deleted = $this->table->delete(['id' => $data['response_id']]);
      if ($deleted === false) {
         $this->logError('Failed to delete response', [
            'data' => $data,
            'error' => $this->table->getLastError()
         ]);
         return ['success' => false, 'result' => 'Failed to delete response'];
      }
      $this->cache->flush();
      return ['success' => true, 'result' => $deleted];
   }
   /**
    * Send notifications for new responses
    */
   protected function sendNotifications(int $item_id, ?int $parent_id, int $response_id, int $user_id): void
   {
      // Notify post author
      $post = get_post($item_id);
      if ($post && $post->post_author != $user_id) {
         JVB()->notification()->addNotification(
            $post->post_author,
            'new_response',
            [
               'message' => 'Someone responded to your post',
               'item_id' => $item_id,
               'response_id' => $response_id
            ]
         );
      }
      // Notify parent response author if this is a reply
      if ($parent_id) {
         $parent = $this->table->get(['id' => $parent_id]);
         if ($parent && $parent->user_id != $user_id) {
            JVB()->notification()->addNotification(
               $parent->user_id,
               'response_reply',
               [
                  'message' => 'Someone replied to your response',
                  'item_id' => $item_id,
                  'response_id' => $response_id
               ]
            );
         }
      }
      return $responses;
   }
   /**
@@ -420,7 +149,7 @@
      // Ordering
      if ($request->has_param('orderby')) {
         $orderby = $request->get_param('orderby');
         $valid = ['created_at', 'updated_at', 'upvotes', 'downvotes'];
         $valid = ['created_at', 'updated_at', 'upvotes', 'downvotes', 'karmic_score'];
         if (in_array($orderby, $valid)) {
            $args['orderby'] = $orderby;
@@ -438,74 +167,26 @@
   }
   /**
    * Get child responses recursively
    */
   protected function getChildren(int $item_id, int $parent_id): array
   {
      return $this->getItemResponse($item_id, ['parent_id' => $parent_id]);
   }
   /**
    * Format response for API output
    */
   protected function formatItem(object $response): array
   protected function formatItem(array $response): array
   {
      $formatted = [
         'id' => (int) $response->id,
         'item_id' => (int) $response->item_id,
         'parent_id' => $response->parent_id ? (int) $response->parent_id : null,
         'response' => $response->response,
         'status' => $response->status,
         'created_at' => $response->created_at,
         'updated_at' => $response->updated_at,
         'children' => $this->getChildren($response->item_id, $response->id),
         'upvotes' => (int) ($response->upvotes ?? 0),
         'downvotes' => (int) ($response->downvotes ?? 0),
         'karma' => (int) ($response->upvotes ?? 0) - (int) ($response->downvotes ?? 0),
         'reply_count' => (int) ($response->reply_count ?? 0)
      ];
      // Handle deleted users
      if ($response->is_user_deleted) {
         $formatted['user'] = [
      if ($response['is_user_deleted']) {
         $response['user'] = [
            'id' => null,
            'name' => '[deleted user]',
            'is_deleted' => true
         ];
      } else {
         // Add artist info
         $artist = jvbContentFromUser($response->user_id);
         if (!empty($artist)) {
            $formatted['artist'] = [
               'name' => $artist['name'],
               'url' => $artist['url'],
               'shop' => $artist['shop'] ?? null
            ];
         }
         $user = get_userdata($response['user_id']);
         $response['user'] = [
            'id'  => $response['user_id'],
            'name'   => $user->display_name,
            'url' => jvbUserProfileLink($response['user_id']),
         ];
      }
      return $formatted;
   }
   /**
    * Handle user deletion by anonymizing responses
    */
   public function handleUserDeletion(int $user_id): void
   {
      $updated = $this->table->update(
         [
            'is_user_deleted' => 1,
            'updated_at' => current_time('mysql')
         ],
         ['user_id' => $user_id]
      );
      $this->logError(
         'Anonymized responses for deleted user',
         ['user_id' => $user_id, 'count' => $updated],
         'info'
      );
      $this->cache->flush();
      return $response;
   }
}