<?php
|
namespace JVBase\rest\routes;
|
|
use JVBase\managers\CustomTable;
|
use JVBase\rest\Response;
|
use JVBase\rest\Rest;
|
use JVBase\rest\Route;
|
use JVBase\utility\Features;
|
use WP_REST_Request;
|
use WP_REST_Response;
|
use WP_Error;
|
use Exception;
|
|
if (!defined('ABSPATH')) {
|
exit; // Exit if accessed directly
|
}
|
class VoteRoutes extends Rest
|
{
|
public function __construct()
|
{
|
$this->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')??'');
|
if ((!Features::forContent($content)->has('karma') && !Features::forTaxonomy($content)->has('karma') && !Features::forUser($content)->has('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 = match(true) {
|
array_key_exists($content, JVB_CONTENT) => 'post',
|
array_key_exists($content, JVB_TAXONOMY) => 'term',
|
array_key_exists($content, JVB_USER) => 'user',
|
default => 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 (jvbGlobalKarma() as $type => $content_types) {
|
foreach ($content_types as $content_type) {
|
$table = CustomTable::for('karma_' . $content_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[$content_type][$vote->item_id] = $vote->vote;
|
}
|
}
|
}
|
}
|
|
$this->cache->set($user, $votes);
|
|
return Response::success($votes);
|
}
|
}
|