cacheName = 'karma'; $this->cacheTtl = DAY_IN_SECONDS; parent::__construct(); add_filter(BASE.'handle_bulk_operation', [$this, 'processOperation'], 10, 3); } /** * Registers vote routes * @return void */ public function registerRoutes():void { Route::for('vote') ->post([$this, 'handleVote']) ->args([ 'user' => 'integer|required', 'id' => 'string|required', 'item_id' => 'integer|required', 'content' => 'string|required', 'vote' => 'string|required|enum:up,down', ]) ->auth('user') ->rateLimit(10) ->get([$this, 'getVotes']) ->args([ 'user' => 'integer', ]) ->auth('user') ->rateLimit(30) ->register(); } /** * @param WP_REST_Request $request * * @return WP_REST_Response */ public function handleVote(WP_REST_Request $request):WP_REST_Response { $content = sanitize_text_field($request->get_param('content')??''); $registrar = Registrar::getInstance($content); if (!$registrar || !$registrar->hasFeature('karma')) { return Response::validationError(['message' => __('Invalid content', 'jvb')]); } $vote = sanitize_text_field($request->get_param('vote')??''); $itemID = absint($request->get_param('item_id')??0); if ($itemID === 0 || !in_array($vote, ['up', 'down'])) { return Response::validationError(['message' => __('Invalid item or vote attempt', 'jvb')]); } $user = absint($request->get_param('user')); if (!$this->userCheck($user)) { return Response::validationError(['message' => __('User doesn\'t match. Bot?', 'jvb')]); } $operation = sanitize_text_field($request->get_param('id')); $type = Registrar::getInstance($content)->getType()??false; if (!$type) { return Response::validationError(['message' => __('Invalid content type', 'jvb')]); } $data = [ 'user' => $user, 'item_id' => $itemID, 'content' => $content, 'type' => $type, 'vote' => $vote, ]; error_log('Final Vote Data: '.print_r($data, true)); error_log('Operation: '.print_r($operation, true)); $operationID = JVB()->queue()->add( 'karmic', $user, $data, [ 'priority' => 'high', 'operation_id' => $operation, ] ); return $this->queued($operationID['operation_id']); } /** * @param WP_Error|array $result * @param object $operation * @param array $data * * @return WP_Error|array * @throws Exception */ public function processOperation(WP_Error|array $result, object $operation, array $data):WP_Error|array { if ($operation->type !== 'karmic') { return $result; } //Check if item exists $item = match ($data['type']) { 'post' => get_post($data['item_id']), 'term' => get_term($data['item_id'], jvbCheckBase($data['content'])), 'user' => get_userdata($data['item_id']), default => false }; if (!$item || is_wp_error($item)) { return [ 'success' => false, 'result' => __('Invalid item', 'jvb') ]; } $table = CustomTable::for('karma_' . $data['content']); return $table->transaction(function($table) use ($data) { // Check existing vote $existing = $table->where([ 'item_id' => $data['item_id'], 'user_id' => $data['user'] ])->first(); $existing_vote = $existing->vote ?? null; $new_vote = $data['vote']; // No previous vote - insert new if ($existing_vote === null) { $inserted = $table->create([ 'item_id' => $data['item_id'], 'user_id' => $data['user'], 'vote' => $new_vote, ]); if (!$inserted) { throw new Exception('Failed to record vote'); } $this->updateVoteCount($data['content'], $data['type'], $data['item_id'], $new_vote, 1); $this->cache->invalidate($data['user']); return [ 'success' => true, 'result' => __('Vote recorded', 'jvb'), ]; } // Changing vote if ($existing_vote !== $new_vote) { $updated = $table->where([ 'item_id' => $data['item_id'], 'user_id' => $data['user'] ])->updateResults(['vote' => $new_vote]); if (!$updated) { throw new Exception('Failed to update vote'); } // Decrement old, increment new $this->updateVoteCount($data['content'], $data['type'], $data['item_id'], $existing_vote, -1); $this->updateVoteCount($data['content'], $data['type'], $data['item_id'], $new_vote, 1); $this->cache->invalidate($data['user']); return [ 'success' => true, 'result' => __('Vote updated', 'jvb'), ]; } // Toggle off - remove vote $deleted = $table->where([ 'item_id' => $data['item_id'], 'user_id' => $data['user'] ])->deleteResults(); if (!$deleted) { throw new Exception('Failed to remove vote'); } $this->updateVoteCount($data['content'], $data['type'], $data['item_id'], $existing_vote, -1); $this->cache->delete($data['user']); return [ 'success' => true, 'result' => __('Vote removed', 'jvb'), ]; }); } /** * @param string $content * @param int $ID * @param string $vote * @param int $value * * @return void */ protected function updateVoteCount(string $content, string $type, int $ID, string $vote, int $value):void { $key = ($vote === 'down') ? BASE.'downvotes' : BASE.'upvotes'; switch ($type) { case 'post': $old = (int) get_post_meta($ID, $key, true); $new = max(0, $old + $value); update_post_meta($ID, $key, $new); $up = (int) get_post_meta($ID, BASE.'upvotes', true); $down = (int) get_post_meta($ID, BASE.'downvotes', true); update_post_meta($ID, BASE.'karma', $up - $down); break; case 'term': $old = (int) get_term_meta($ID, $key, true); $new = max(0, $old + $value); update_term_meta($ID, $key, $new); $up = (int) get_term_meta($ID, BASE.'upvotes', true); $down = (int) get_term_meta($ID, BASE.'downvotes', true); update_term_meta($ID, BASE.'karma', $up - $down); break; case 'user': $old = (int) get_user_meta($ID, $key, true); $new = max(0, $old + $value); update_user_meta($ID, $key, $new); $up = (int) get_user_meta($ID, BASE.'upvotes', true); $down = (int) get_user_meta($ID, BASE.'downvotes', true); update_user_meta($ID, BASE.'karma', $up - $down); break; case 'response': $field = str_replace(BASE, '', $key); CustomTable::for('responses')->query( "UPDATE {table} SET $field = GREATEST(0, $field + %d), karma = (upvotes - downvotes) WHERE id = %d", [$value, $ID] ); break; } } /** * @param WP_REST_Request $request * * @return WP_REST_Response */ public function getVotes(WP_REST_Request $request): WP_REST_Response { $user = absint($request->get_param('user') ?? get_current_user_id()); $cache = $this->cache->get($user); if ($cache) { return Response::success($cache); } $votes = []; foreach (Registrar::getFeatured('has_karma') as $type) { $table = CustomTable::for('karma_' . $type); // Skip if table doesn't exist global $wpdb; if ($wpdb->get_var("SHOW TABLES LIKE '{$table->getFullTableName()}'") != $table->getFullTableName()) { continue; } $results = $table->where(['user_id' => $user])->getResults(); if (!empty($results)) { foreach ($results as $vote) { $votes[$type][$vote->item_id] = $vote->vote; } } } $this->cache->set($user, $votes); return Response::success($votes); } }