key = $key; if (is_null($references)) { $references = match($key){ 'post' => 'post', 'term' => 'term', 'user' => 'user', default => false }; if (!$references) { error_log('[KarmaManager]::__construct No references set, and no default found. Could not create instance'); unset(self::$instances[$key]); return; } } $this->references = $references; $test = $this->defineTable(); if (!$test) { unset(self::$instances[$key]); } // $this->cache = Cache::for($this->references.'_karma'); // switch ($this->references) { // case 'post': // $this->cache->connect('post'); // break; // case 'term': // $this->cache->connect('term'); // break; // case 'user': // $this->cache->connect('user'); // break; // } if (!isset(self::$cache)) { self::$cache = Cache::for('user_karma')->connect('user'); } add_filter(BASE.'handle_bulk_operation', [$this, 'processOperation'], 10, 3); } public static function getInstance(string $key):KarmaManager|false { if (!isset(self::$instances[$key])) { return false; } return self::$instances[$key]; } public static function for(string $key, ?string $references = null): KarmaManager { $key = sanitize_key($key); if (!isset(self::$instances[$key])) { self::$instances[$key] = new KarmaManager($key, $references); } return self::$instances[$key]; } protected function getReferenceTable(CustomTable $table):array { switch ($this->references) { case 'post': case 'posts': return [ $table->getPostIDType(), $table->getPostTable(), 'ID' ]; case 'term': case 'terms': return [ $table->getTermIDType(), $table->getTermTable(), 'term_id' ]; case 'user': case 'users': return [ $table->getUserIDType(), $table->getUserTable(), 'ID' ]; } //If we get here, it is a custom table. $custom = CustomTable::getInstance($this->references); if (!$custom) { return [false,false,false]; } return [ 'bigint(20)', $custom->getFullTableName(), 'id' ]; } protected function defineTable():bool { $table = CustomTable::for('karma_'.$this->key); [$type, $referenceTable, $column] = $this->getReferenceTable($table); if (!$type) { error_log('[KarmaManager]::defineTable Attempted to build reference for invalid table: '.$this->references); CustomTable::destroyInstance('karma_'.$this->key); return false; } $table->setColumns([ 'id' => 'bigint(20) unsigned NOT NULL AUTO_INCREMENT', 'item_id' => "{$type} NOT NULL", 'user_id' => "{$table->getUserIDType()} NOT NULL", 'content' => 'varchar(255) NOT NULL', 'vote' => "ENUM('up','down') NOT NULL", 'created_at'=> 'datetime NOT NULL DEFAULT CURRENT_TIMESTAMP', 'updated_at'=> 'datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP' ]); $table->setKeys([ ['key' => 'PRIMARY', 'value' => 'id'], ['key' => 'UNIQUE', 'value' => '`user_item` (`user_id`, `item_id`, `content`)'], '`item_vote` (`item_id`,`content`,`vote`)', '`item_id` (`item_id`)', '`user_id` (`user_id`)' ]); $base = BASE; $table->setConstraints([ "CONSTRAINT `{$base}kt_{$this->key}_{$type}_item_id` FOREIGN KEY (`item_id`) REFERENCES `{$referenceTable}` (`{$column}`) ON DELETE CASCADE", "CONSTRAINT `{$base}kt_{$this->key}_{$type}_user_id` FOREIGN KEY (`user_id`) REFERENCES `{$table->getUserTable()}` (`ID`) ON DELETE CASCADE" ]); $table->defineTable(); $this->table = $table; return true; } protected function response(?bool $success = null, string $message =''):array { $response = [ 'success' => is_null($success) ? 'partial' : $success, ]; if (!empty($message)) { $response['message'] = $message; } return $response; } protected function getItem(int $itemID, string $content):object|false { $based = jvbCheckBase($content); return match($this->references) { 'post' => get_post($itemID), 'term' => get_term($itemID, $based), 'user' => get_userdata($itemID), 'response' => JVB()->responses()->response->get(['id' => $itemID])??false, default => false //TODO if we do have custom tables, we'll have to set that up here }; } public function vote(int $userID, int $itemID, string $content, ?bool $vote = null):array { $content = jvbNoBase($content); $item = $this->getItem($itemID, $content); if (!$item || is_wp_error($item)) { return $this->response(false, 'Could not find target'); } $message = ''; //removing record if (is_null($vote)){ $success = $this->table->delete([ 'user_id' => $userID, 'item_id' => $itemID, 'content' => $content ]); if ($success) { $message = 'Vote removed successfully'; } } else { $success = $this->table->findOrCreate( [ 'user_id' => $userID, 'item_id' => $itemID, 'content' => $content ], [ 'vote' => $vote ? 'up' : 'down' ] ); if (!$success) { $message = 'Could not cast your vote'; } else { $message = 'Vote cast successfully'; } } self::$cache->forget($userID); JVB()->queue()->queueOperation( 'karmic', 0, [ 'items' => [ 'itemID' => $itemID, 'content' => $content, ], 'type' => $this->references, ], [ 'chunk_key' => 'items', ] ); return $this->response((bool) $success, $message); } public function getCount(int $itemID, string $content):array { $upvotes = count($this->table->pluck( 'id', [ 'item_id' => $itemID, 'content' => $content, 'vote' => 'up' ])); $downvotes = count($this->table->pluck( 'id', [ 'item_id' => $itemID, 'content' => $content, 'vote' => 'down' ] )); return [ 'upvotes' => $upvotes, 'downvotes' => $downvotes, 'karmic_score' => $upvotes - $downvotes ]; } public function processOperation(WP_Error|array $result, object $operation, array $data):WP_Error|array { if ($operation->type !== 'karmic') { return $result; } if ($data['type'] !== $this->references) { return $result; } $results = []; foreach ($data['items'] as $item) { [$upvotes, $downvotes, $karma] = $this->getCount($item['itemID'], $item['content']); try { switch ($this->references) { case 'post': update_post_meta($item['itemID'], BASE.'upvotes', $upvotes); update_post_meta($item['itemID'], BASE.'downvotes', $downvotes); update_post_meta($item['itemID'], BASE.'karma', $karma); break; case 'term': update_term_meta($item['itemID'], BASE.'upvotes', $upvotes); update_term_meta($item['itemID'], BASE.'downvotes', $downvotes); update_term_meta($item['itemID'], BASE.'karma', $karma); break; case 'user': update_user_meta($item['itemID'], BASE.'upvotes', $upvotes); update_user_meta($item['itemID'], BASE.'downvotes', $downvotes); update_user_meta($item['itemID'], BASE.'karma', $karma); break; case 'response': JVB()->responses()->response->update( [ 'upvotes' => $upvotes, 'downvotes' => $downvotes, 'karma' => $karma ], [ 'id' => $item['itemID'] ] ); break; } $results[$item['itemID']] = true; } catch (Exception $e) { $results[$item['itemID']] = false; } } return [ 'success' => true, 'result' => $results ]; } public static function getUserVotes(int $userID):array { return self::$cache->remember( $userID, function() use($userID) { $votes = []; foreach (self::$instances as $instance) { $results = $instance->table->getMany([ 'where' => [ 'user_id' => $userID ] ]); foreach ($results as $result) { if (!array_key_exists($result['content'], $votes)) { $votes[$result['content']] = []; } $votes[$result['content']][$result['item_id']] = $result['vote']; } } return $votes; } ); } }