query( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Custom table; the unique key keeps one row per reference. $wpdb->prepare( 'INSERT IGNORE INTO %i (user_id, points, reason, ref, object_type, object_id, note, created_by, created_at) VALUES (%d, %d, %s, %s, %s, %d, %s, %d, %s)', Tables::name( 'points' ), $user_id, (int) $points, $reason, substr( (string) $ref, 0, 100 ), substr( sanitize_key( $object_type ), 0, 20 ), absint( $object_id ), mb_substr( (string) ( $extra['note'] ?? '' ), 0, 191 ), absint( $extra['created_by'] ?? 0 ), (string) ( $extra['created_at'] ?? Time::now() ) ) ); if ( ! $added ) { return false; } $after = self::recount( $user_id ); /** * Fires after a member gets points (also 0 points, for counted actions). * * @since 1.0.0 * * @param int $user_id Member. * @param int $points Points of this action. * @param string $reason Reason. * @param int $before Total before. * @param int $after Total after. */ do_action( 'spacebay_points_added', $user_id, (int) $points, $reason, $before, $after ); return true; } /** * Takes back the points of one reference. * * @param int $user_id Member. * @param string $ref Reference. * @return bool Whether a row was removed. */ public static function remove( $user_id, $ref ) { global $wpdb; $user_id = absint( $user_id ); $removed = $wpdb->delete( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Custom table. Tables::name( 'points' ), array( 'user_id' => $user_id, 'ref' => (string) $ref, ) ); if ( $removed ) { self::recount( $user_id ); } return (bool) $removed; } /** * Takes back every reference that starts with a prefix, for any member. * * Used when a post goes away: "like:post:12:" removes the points its * author got for each like. * * @param string $prefix Reference prefix. * @return void */ public static function remove_prefix( $prefix ) { global $wpdb; $table = Tables::name( 'points' ); $like = $wpdb->esc_like( (string) $prefix ) . '%'; $users = $wpdb->get_col( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Fresh read before a delete. $wpdb->prepare( 'SELECT DISTINCT user_id FROM %i WHERE ref LIKE %s', $table, $like ) ); if ( ! $users ) { return; } $wpdb->query( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Custom table. $wpdb->prepare( 'DELETE FROM %i WHERE ref LIKE %s', $table, $like ) ); foreach ( $users as $user_id ) { self::recount( (int) $user_id ); } } /** * Takes back one reference for whoever has it. * * @param string $ref Reference. * @return void */ public static function remove_ref( $ref ) { global $wpdb; $table = Tables::name( 'points' ); $users = $wpdb->get_col( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Fresh read before a delete. $wpdb->prepare( 'SELECT user_id FROM %i WHERE ref = %s', $table, (string) $ref ) ); foreach ( (array) $users as $user_id ) { self::remove( (int) $user_id, $ref ); } } /** * A member's total from the profile row. * * @param int $user_id Member. * @return int */ public static function total( $user_id ) { $profile = Profiles::get( $user_id ); return $profile ? (int) $profile['points'] : 0; } /** * Adds up a member's rows and stores the total on the profile. * * @param int $user_id Member. * @return int New total. */ public static function recount( $user_id ) { global $wpdb; $user_id = absint( $user_id ); // A member who is being deleted keeps no profile row. if ( ! get_userdata( $user_id ) ) { return 0; } Profiles::ensure( $user_id ); $total = (int) $wpdb->get_var( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Fresh sum after a write. $wpdb->prepare( 'SELECT COALESCE(SUM(points), 0) FROM %i WHERE user_id = %d', Tables::name( 'points' ), $user_id ) ); $wpdb->update( Tables::name( 'profiles' ), array( 'points' => $total ), array( 'user_id' => $user_id ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Custom table. Cache::flush( 'profiles' ); return $total; } /** * How many rows of each reason a member has. Badges use these counts. * * @param int $user_id Member. * @return array */ public static function counts( $user_id ) { global $wpdb; $rows = $wpdb->get_results( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Fresh counts for badge checks. $wpdb->prepare( 'SELECT reason, COUNT(id) AS total FROM %i WHERE user_id = %d GROUP BY reason', Tables::name( 'points' ), absint( $user_id ) ), ARRAY_A ); $counts = array_fill_keys( self::REASONS, 0 ); foreach ( (array) $rows as $row ) { $counts[ $row['reason'] ] = (int) $row['total']; } return $counts; } /** * A member's latest point changes, newest first. * * @param int $user_id Member. * @param int $page Page, from 1. * @param int $per_page Rows per page. * @return array{items: array[], total: int} */ public static function history( $user_id, $page = 1, $per_page = 20 ) { global $wpdb; $table = Tables::name( 'points' ); $per_page = max( 1, min( 50, absint( $per_page ) ) ); $offset = ( max( 1, absint( $page ) ) - 1 ) * $per_page; // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Custom table, paged. $rows = $wpdb->get_results( $wpdb->prepare( 'SELECT id, points, reason, object_type, object_id, note, created_at FROM %i WHERE user_id = %d AND points <> 0 ORDER BY created_at DESC, id DESC LIMIT %d OFFSET %d', $table, absint( $user_id ), $per_page, $offset ), ARRAY_A ); $total = (int) $wpdb->get_var( $wpdb->prepare( 'SELECT COUNT(id) FROM %i WHERE user_id = %d AND points <> 0', $table, absint( $user_id ) ) ); // phpcs:enable $items = array(); foreach ( (array) $rows as $row ) { $items[] = array( 'id' => (int) $row['id'], 'points' => (int) $row['points'], 'reason' => $row['reason'], 'label' => self::label( $row['reason'], (int) $row['points'] ), 'note' => (string) $row['note'], 'object_type' => $row['object_type'], 'object_id' => (int) $row['object_id'], 'created_at' => Time::iso( $row['created_at'] ), ); } return array( 'items' => $items, 'total' => $total, ); } /** * Words for one reason, shown in the points history. * * @param string $reason Reason. * @param int $points Points. * @return string */ public static function label( $reason, $points ) { switch ( $reason ) { case 'post': return __( 'Published a post', 'spacebay' ); case 'comment': return __( 'Wrote a comment', 'spacebay' ); case 'like': return __( 'Got a like', 'spacebay' ); case 'daily': return __( 'Visited the community', 'spacebay' ); case 'event': return __( 'Going to an event', 'spacebay' ); case 'manual': return $points < 0 ? __( 'Points taken away by the community team', 'spacebay' ) : __( 'Points from the community team', 'spacebay' ); } return ''; } /** * Removes every point of a member. * * @param int $user_id Member. * @return void */ public static function delete_for_user( $user_id ) { global $wpdb; $wpdb->delete( Tables::name( 'points' ), array( 'user_id' => absint( $user_id ) ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Custom table. } }