Snapshot: Krystie uncommitted edits on sami-ahmed.net as of 2026-05-20 14:56 PT
These changes were applied DIRECTLY to the production cPanel server via Fileman/save_file_content API calls between ~10:30 PT and 14:44 PT today. None of them were committed or pushed at the time, violating the Software → Repo rule (CLAUDE.md, added 2026-05-18). Captured by Claude (VSCode-side) at Sami request so the changes can be reviewed/cherry-picked instead of being silently lost. Notable concerns: - class-beep.php shrank from 3387 → 1395 bytes (likely truncation, not edit) - beep.css shrank from 35431 → 22948 bytes (-12.5 KB; ~half the file gone) - class-replies.php shrank from 8793 → 6406 bytes - 3 new files (class-magic-links.php, class-follows.php, create_poll_tables.php) were created server-only with no repo presence Source: live cPanel snapshot at /tmp/beep-snapshot-2026-05-20/
This commit is contained in:
+2
-54
@@ -14,62 +14,13 @@ class Beep_Plugin {
|
||||
Beep_Shortcode::init();
|
||||
|
||||
add_action('wp_enqueue_scripts', [__CLASS__, 'enqueue_assets']);
|
||||
|
||||
// Set default Giphy API key if not already set
|
||||
if (!get_option('beep_giphy_key')) {
|
||||
update_option('beep_giphy_key', 'xuxYxuIvzPqw5tPB4laa7sQO8dFtSlNS');
|
||||
}
|
||||
|
||||
// Admin settings
|
||||
add_action('admin_menu', [__CLASS__, 'add_admin_menu']);
|
||||
add_action('admin_init', [__CLASS__, 'register_settings']);
|
||||
}
|
||||
|
||||
public static function add_admin_menu() {
|
||||
add_options_page(
|
||||
'Beep Settings',
|
||||
'Beep',
|
||||
'manage_options',
|
||||
'beep-settings',
|
||||
[__CLASS__, 'render_settings_page']
|
||||
);
|
||||
}
|
||||
|
||||
public static function register_settings() {
|
||||
register_setting('beep_settings_group', 'beep_giphy_key', [
|
||||
'sanitize_callback' => 'sanitize_text_field',
|
||||
]);
|
||||
add_settings_section('beep_main_section', 'API Configuration', null, 'beep-settings');
|
||||
add_settings_field(
|
||||
'beep_giphy_key',
|
||||
'Giphy API Key',
|
||||
[__CLASS__, 'giphy_key_field'],
|
||||
'beep-settings',
|
||||
'beep_main_section',
|
||||
['label_for' => 'beep_giphy_key']
|
||||
);
|
||||
}
|
||||
|
||||
public static function giphy_key_field() {
|
||||
$key = get_option('beep_giphy_key', '');
|
||||
echo '<input type="text" id="beep_giphy_key" name="beep_giphy_key" value="' . esc_attr($key) . '" class="regular-text" placeholder="Enter your Giphy API key">';
|
||||
echo '<p class="description">Get a free API key at <a href="https://developers.giphy.com/" target="_blank">developers.giphy.com</a></p>';
|
||||
}
|
||||
|
||||
public static function render_settings_page() {
|
||||
if (!current_user_can('manage_options')) return;
|
||||
echo '<div class="wrap"><h1>Beep Settings</h1>';
|
||||
echo '<form method="post" action="options.php">';
|
||||
settings_fields('beep_settings_group');
|
||||
do_settings_sections('beep-settings');
|
||||
submit_button();
|
||||
echo '</form></div>';
|
||||
}
|
||||
|
||||
public static function activate() {
|
||||
Beep_CPT::register();
|
||||
Beep_Likes::create_table();
|
||||
Beep_Polls::create_tables();
|
||||
Beep_Magic_Links::create_table();
|
||||
flush_rewrite_rules();
|
||||
}
|
||||
|
||||
@@ -91,15 +42,12 @@ class Beep_Plugin {
|
||||
BEEP_VERSION,
|
||||
true
|
||||
);
|
||||
$giphy_key = get_option('beep_giphy_key', '');
|
||||
|
||||
wp_localize_script('beep', 'BeepConfig', [
|
||||
'restUrl' => esc_url_raw(rest_url('beep/v1/')),
|
||||
'nonce' => wp_create_nonce('wp_rest'),
|
||||
'charLimit' => BEEP_CHAR_LIMIT,
|
||||
'loginUrl' => esc_url_raw(wp_login_url(is_singular() ? get_permalink() : home_url('/'))),
|
||||
'isLoggedIn' => is_user_logged_in(),
|
||||
'giphyKey' => $giphy_key,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,207 @@
|
||||
<?php
|
||||
/**
|
||||
* Beep Follow System — class-follows.php
|
||||
*
|
||||
* REST endpoints for following/unfollowing users.
|
||||
*/
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
class Beep_Follows {
|
||||
|
||||
const REST_NAMESPACE = 'beep/v1';
|
||||
|
||||
public function register_routes() {
|
||||
// Follow a user: POST /beep/v1/follow/<user_id>
|
||||
register_rest_route(self::REST_NAMESPACE, '/follow/(?P<user_id>\d+)', [
|
||||
'methods' => 'POST',
|
||||
'callback' => [$this, 'follow_user'],
|
||||
'permission_callback' => [$this, 'require_login'],
|
||||
]);
|
||||
|
||||
// Unfollow a user: DELETE /beep/v1/follow/<user_id>
|
||||
register_rest_route(self::REST_NAMESPACE, '/follow/(?P<user_id>\d+)', [
|
||||
'methods' => 'DELETE',
|
||||
'callback' => [$this, 'unfollow_user'],
|
||||
'permission_callback' => [$this, 'require_login'],
|
||||
]);
|
||||
|
||||
// Check follow status: GET /beep/v1/follow/<user_id>
|
||||
register_rest_route(self::REST_NAMESPACE, '/follow/(?P<user_id>\d+)', [
|
||||
'methods' => 'GET',
|
||||
'callback' => [$this, 'check_follow'],
|
||||
'permission_callback' => '__return_true',
|
||||
]);
|
||||
|
||||
// Get users I'm following: GET /beep/v1/following
|
||||
register_rest_route(self::REST_NAMESPACE, '/following', [
|
||||
'methods' => 'GET',
|
||||
'callback' => [$this, 'get_following'],
|
||||
'permission_callback' => [$this, 'require_login'],
|
||||
]);
|
||||
|
||||
// Get my followers: GET /beep/v1/followers
|
||||
register_rest_route(self::REST_NAMESPACE, '/followers', [
|
||||
'methods' => 'GET',
|
||||
'callback' => [$this, 'get_followers'],
|
||||
'permission_callback' => [$this, 'require_login'],
|
||||
]);
|
||||
}
|
||||
|
||||
public function require_login() {
|
||||
return is_user_logged_in() ? true : new WP_Error('rest_not_logged_in', 'You must be logged in', ['status' => 401]);
|
||||
}
|
||||
|
||||
public function follow_user($request) {
|
||||
$user_id = intval($request->get_param('user_id'));
|
||||
$follower_id = get_current_user_id();
|
||||
|
||||
if ($user_id === $follower_id) {
|
||||
return new WP_Error('rest_cannot_follow_self', 'You cannot follow yourself', ['status' => 400]);
|
||||
}
|
||||
|
||||
$target = get_userdata($user_id);
|
||||
if (!$target) {
|
||||
return new WP_Error('rest_user_not_found', 'User not found', ['status' => 404]);
|
||||
}
|
||||
|
||||
global $wpdb;
|
||||
$table = $wpdb->prefix . 'beep_follows';
|
||||
|
||||
$existing = $wpdb->get_row($wpdb->prepare(
|
||||
"SELECT ID FROM `$table` WHERE follower_id = %d AND following_id = %d",
|
||||
$follower_id, $user_id
|
||||
));
|
||||
|
||||
if ($existing) {
|
||||
return ['status' => 'following', 'user_id' => $user_id, 'display_name' => $target->display_name];
|
||||
}
|
||||
|
||||
$wpdb->insert($table, [
|
||||
'follower_id' => $follower_id,
|
||||
'following_id' => $user_id,
|
||||
], ['%d', '%d']);
|
||||
|
||||
return ['status' => 'following', 'user_id' => $user_id, 'display_name' => $target->display_name];
|
||||
}
|
||||
|
||||
public function unfollow_user($request) {
|
||||
$user_id = intval($request->get_param('user_id'));
|
||||
$follower_id = get_current_user_id();
|
||||
|
||||
global $wpdb;
|
||||
$table = $wpdb->prefix . 'beep_follows';
|
||||
|
||||
$wpdb->delete($table, [
|
||||
'follower_id' => $follower_id,
|
||||
'following_id' => $user_id,
|
||||
], ['%d', '%d']);
|
||||
|
||||
return ['status' => 'unfollowed', 'user_id' => $user_id];
|
||||
}
|
||||
|
||||
public function check_follow($request) {
|
||||
$user_id = intval($request->get_param('user_id'));
|
||||
$current_user = get_current_user_id();
|
||||
|
||||
global $wpdb;
|
||||
$table = $wpdb->prefix . 'beep_follows';
|
||||
|
||||
$is_following = false;
|
||||
if ($current_user) {
|
||||
$is_following = (bool) $wpdb->get_var($wpdb->prepare(
|
||||
"SELECT ID FROM `$table` WHERE follower_id = %d AND following_id = %d",
|
||||
$current_user, $user_id
|
||||
));
|
||||
}
|
||||
|
||||
$target = get_userdata($user_id);
|
||||
if (!$target) {
|
||||
return new WP_Error('rest_user_not_found', 'User not found', ['status' => 404]);
|
||||
}
|
||||
|
||||
$follower_count = intval($wpdb->get_var($wpdb->prepare(
|
||||
"SELECT COUNT(*) FROM `$table` WHERE following_id = %d", $user_id
|
||||
)));
|
||||
|
||||
$following_count = intval($wpdb->get_var($wpdb->prepare(
|
||||
"SELECT COUNT(*) FROM `$table` WHERE follower_id = %d", $user_id
|
||||
)));
|
||||
|
||||
return [
|
||||
'is_following' => $is_following,
|
||||
'user_id' => $user_id,
|
||||
'display_name' => $target->display_name,
|
||||
'user_login' => $target->user_login,
|
||||
'follower_count' => $follower_count,
|
||||
'following_count' => $following_count,
|
||||
];
|
||||
}
|
||||
|
||||
public function get_following($request) {
|
||||
$user_id = get_current_user_id();
|
||||
|
||||
global $wpdb;
|
||||
$table = $wpdb->prefix . 'beep_follows';
|
||||
|
||||
$following = $wpdb->get_results($wpdb->prepare(
|
||||
"SELECT u.ID, u.display_name, u.user_login, f.created_at
|
||||
FROM `$table` f
|
||||
JOIN {$wpdb->users} u ON u.ID = f.following_id
|
||||
WHERE f.follower_id = %d
|
||||
ORDER BY f.created_at DESC",
|
||||
$user_id
|
||||
), ARRAY_A);
|
||||
|
||||
return ['following' => $following, 'count' => count($following)];
|
||||
}
|
||||
|
||||
public function get_followers($request) {
|
||||
$user_id = get_current_user_id();
|
||||
|
||||
global $wpdb;
|
||||
$table = $wpdb->prefix . 'beep_follows';
|
||||
|
||||
$followers = $wpdb->get_results($wpdb->prepare(
|
||||
"SELECT u.ID, u.display_name, u.user_login, f.created_at
|
||||
FROM `$table` f
|
||||
JOIN {$wpdb->users} u ON u.ID = f.follower_id
|
||||
WHERE f.following_id = %d
|
||||
ORDER BY f.created_at DESC",
|
||||
$user_id
|
||||
), ARRAY_A);
|
||||
|
||||
return ['followers' => $followers, 'count' => count($followers)];
|
||||
}
|
||||
|
||||
public static function is_following($follower_id, $following_id) {
|
||||
global $wpdb;
|
||||
$table = $wpdb->prefix . 'beep_follows';
|
||||
return (bool) $wpdb->get_var($wpdb->prepare(
|
||||
"SELECT ID FROM `$table` WHERE follower_id = %d AND following_id = %d",
|
||||
$follower_id, $following_id
|
||||
));
|
||||
}
|
||||
|
||||
public static function get_follower_count($user_id) {
|
||||
global $wpdb;
|
||||
$table = $wpdb->prefix . 'beep_follows';
|
||||
return intval($wpdb->get_var($wpdb->prepare(
|
||||
"SELECT COUNT(*) FROM `$table` WHERE following_id = %d", $user_id
|
||||
)));
|
||||
}
|
||||
|
||||
public static function get_following_count($user_id) {
|
||||
global $wpdb;
|
||||
$table = $wpdb->prefix . 'beep_follows';
|
||||
return intval($wpdb->get_var($wpdb->prepare(
|
||||
"SELECT COUNT(*) FROM `$table` WHERE follower_id = %d", $user_id
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
// Bootstrap — register routes
|
||||
add_action('rest_api_init', function() {
|
||||
$f = new Beep_Follows();
|
||||
$f->register_routes();
|
||||
});
|
||||
@@ -0,0 +1,234 @@
|
||||
<?php
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
class Beep_Magic_Links {
|
||||
|
||||
const TABLE = 'beep_magic_links';
|
||||
const TOKEN_LENGTH = 32;
|
||||
const EXPIRY_HOURS = 24;
|
||||
|
||||
public static function init() {
|
||||
// Auto-create table on first load if needed
|
||||
self::maybe_create_table();
|
||||
|
||||
add_action('rest_api_init', [__CLASS__, 'register_rest_routes']);
|
||||
add_action('init', [__CLASS__, 'handle_magic_link_login']);
|
||||
add_action('wp', [__CLASS__, 'cleanup_expired_tokens']);
|
||||
}
|
||||
|
||||
private static function maybe_create_table() {
|
||||
static $checked = false;
|
||||
if ($checked) {
|
||||
return;
|
||||
}
|
||||
$checked = true;
|
||||
|
||||
global $wpdb;
|
||||
$table = $wpdb->prefix . self::TABLE;
|
||||
if ($wpdb->get_var("SHOW TABLES LIKE '$table'") !== $table) {
|
||||
self::create_table();
|
||||
}
|
||||
}
|
||||
|
||||
public static function create_table() {
|
||||
global $wpdb;
|
||||
$table = $wpdb->prefix . self::TABLE;
|
||||
$charset = $wpdb->get_charset_collate();
|
||||
|
||||
$sql = "CREATE TABLE IF NOT EXISTS {$table} (
|
||||
id bigint(20) unsigned NOT NULL auto_increment,
|
||||
user_id bigint(20) unsigned NOT NULL,
|
||||
token varchar(64) NOT NULL,
|
||||
email varchar(100) NOT NULL,
|
||||
created_at datetime NOT NULL default CURRENT_TIMESTAMP,
|
||||
expires_at datetime NOT NULL,
|
||||
used_at datetime DEFAULT NULL,
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY token (token),
|
||||
KEY user_id (user_id),
|
||||
KEY expires_at (expires_at)
|
||||
) {$charset};";
|
||||
|
||||
require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
|
||||
dbDelta($sql);
|
||||
}
|
||||
|
||||
public static function register_rest_routes() {
|
||||
register_rest_route('beep/v1', '/magic-link', [
|
||||
'methods' => 'POST',
|
||||
'callback' => [__CLASS__, 'request_magic_link'],
|
||||
'permission_callback' => '__return_true',
|
||||
]);
|
||||
}
|
||||
|
||||
public static function request_magic_link($request) {
|
||||
$email = sanitize_email($request->get_param('email'));
|
||||
|
||||
if (!$email || !is_email($email)) {
|
||||
return new WP_Error('invalid_email', 'Please enter a valid email address.', ['status' => 400]);
|
||||
}
|
||||
|
||||
$user = get_user_by('email', $email);
|
||||
if (!$user) {
|
||||
// Don't reveal whether an email exists
|
||||
return ['success' => true, 'message' => 'If that email is registered, a login link has been sent.'];
|
||||
}
|
||||
|
||||
// Check rate limit (one request per 5 minutes per email)
|
||||
$rate_key = 'beep_ml_' . md5($email);
|
||||
$last_request = get_transient($rate_key);
|
||||
if ($last_request) {
|
||||
return new WP_Error(
|
||||
'rate_limited',
|
||||
'Please wait a few minutes before requesting another login link.',
|
||||
['status' => 429]
|
||||
);
|
||||
}
|
||||
|
||||
// Generate token
|
||||
$token = wp_generate_password(self::TOKEN_LENGTH, false);
|
||||
$hashed_token = hash('sha256', $token);
|
||||
$expires_at = date('Y-m-d H:i:s', time() + (self::EXPIRY_HOURS * 3600));
|
||||
|
||||
// Store in database
|
||||
global $wpdb;
|
||||
$table = $wpdb->prefix . self::TABLE;
|
||||
|
||||
$wpdb->insert($table, [
|
||||
'user_id' => $user->ID,
|
||||
'token' => $hashed_token,
|
||||
'email' => $email,
|
||||
'expires_at' => $expires_at,
|
||||
]);
|
||||
|
||||
// Set rate limit
|
||||
set_transient($rate_key, true, 5 * 60);
|
||||
|
||||
// Build magic link
|
||||
$login_url = add_query_arg([
|
||||
'beep_magic' => $token,
|
||||
'uid' => $user->ID,
|
||||
], home_url('/'));
|
||||
|
||||
// Email content
|
||||
$site_name = get_bloginfo('name');
|
||||
$subject = sprintf('[%s] Your login link', $site_name);
|
||||
$message = sprintf(
|
||||
"Hi %s,\n\nClick the link below to sign in to %s:\n\n%s\n\nThis link expires in %d hours.\n\nIf you didn't request this, you can ignore this email.\n\n— The %s team",
|
||||
$user->display_name,
|
||||
$site_name,
|
||||
$login_url,
|
||||
self::EXPIRY_HOURS,
|
||||
$site_name
|
||||
);
|
||||
|
||||
// Send email
|
||||
$sent = wp_mail($email, $subject, $message);
|
||||
|
||||
if (!$sent) {
|
||||
return new WP_Error('email_failed', 'Failed to send email. Please try again.', ['status' => 500]);
|
||||
}
|
||||
|
||||
return ['success' => true, 'message' => 'Check your email for the login link!'];
|
||||
}
|
||||
|
||||
public static function handle_magic_link_login() {
|
||||
if (!isset($_GET['beep_magic']) || !isset($_GET['uid'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
$token = sanitize_text_field($_GET['beep_magic']);
|
||||
$user_id = (int) $_GET['uid'];
|
||||
|
||||
if (!$token || !$user_id) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Verify token
|
||||
global $wpdb;
|
||||
$table = $wpdb->prefix . self::TABLE;
|
||||
$hashed_token = hash('sha256', $token);
|
||||
|
||||
$record = $wpdb->get_row($wpdb->prepare(
|
||||
"SELECT * FROM {$table} WHERE token = %s AND user_id = %d AND used_at IS NULL AND expires_at > %s",
|
||||
$hashed_token,
|
||||
$user_id,
|
||||
current_time('mysql')
|
||||
));
|
||||
|
||||
if (!$record) {
|
||||
wp_die('This login link is invalid or has expired. Please request a new one.');
|
||||
}
|
||||
|
||||
// Mark as used
|
||||
$wpdb->update(
|
||||
$table,
|
||||
['used_at' => current_time('mysql')],
|
||||
['id' => $record->id]
|
||||
);
|
||||
|
||||
// Log the user in
|
||||
$user = get_user_by('id', $user_id);
|
||||
if ($user) {
|
||||
wp_set_current_user($user_id);
|
||||
wp_set_auth_cookie($user_id);
|
||||
do_action('wp_login', $user->user_login, $user);
|
||||
|
||||
// Redirect to home or beep page
|
||||
$redirect_url = isset($_GET['redirect_to']) ? esc_url_raw($_GET['redirect_to']) : home_url('/');
|
||||
wp_redirect($redirect_url);
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
public static function cleanup_expired_tokens() {
|
||||
// Run once per session
|
||||
static $run = false;
|
||||
if ($run) {
|
||||
return;
|
||||
}
|
||||
$run = true;
|
||||
|
||||
global $wpdb;
|
||||
$table = $wpdb->prefix . self::TABLE;
|
||||
$wpdb->query($wpdb->prepare(
|
||||
"DELETE FROM {$table} WHERE expires_at < %s OR (used_at IS NOT NULL AND DATE_ADD(used_at, INTERVAL 1 HOUR) < %s)",
|
||||
current_time('mysql'),
|
||||
current_time('mysql')
|
||||
));
|
||||
}
|
||||
|
||||
public static function render_login_form() {
|
||||
ob_start();
|
||||
?>
|
||||
<div class="beep-magic-login">
|
||||
<div class="beep-magic-login-card">
|
||||
<div class="beep-magic-login-header">
|
||||
<svg viewBox="0 0 24 24" width="32" height="32" fill="#1D9BF0">
|
||||
<path d="M12 1.726c-6.585 0-10 5.589-10 7.5S5.415 16.726 12 16.726s10-2.589 10-7.5S18.585 1.726 12 1.726zM12 15.726c-5.33 0-8-2.91-8-4.5S6.67 6.726 12 6.726s8 2.91 8 4.5-2.67 4.5-8 4.5z"/>
|
||||
</svg>
|
||||
<h3>Sign in to Beep</h3>
|
||||
<p>Enter your email to receive a magic login link</p>
|
||||
</div>
|
||||
<form class="beep-magic-login-form" data-beep-magic-form>
|
||||
<input type="email" name="email" class="beep-magic-email" placeholder="your@email.com" required>
|
||||
<button type="submit" class="beep-button beep-magic-submit" data-beep-magic-submit>
|
||||
<span class="beep-magic-btn-text">Send Login Link</span>
|
||||
<span class="beep-magic-loading" hidden>Sending...</span>
|
||||
</button>
|
||||
</form>
|
||||
<div class="beep-magic-success" data-beep-magic-success hidden>
|
||||
<svg viewBox="0 0 24 24" width="24" height="24" fill="#00BA7C">
|
||||
<path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z"/>
|
||||
</svg>
|
||||
<span>Check your email for the login link!</span>
|
||||
</div>
|
||||
<div class="beep-magic-error" data-beep-magic-error hidden></div>
|
||||
</div>
|
||||
</div>
|
||||
<?php
|
||||
return ob_get_clean();
|
||||
}
|
||||
}
|
||||
+102
-143
@@ -1,5 +1,7 @@
|
||||
<?php
|
||||
if (!defined('ABSPATH')) { exit; }
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
class Beep_Replies {
|
||||
|
||||
@@ -11,77 +13,140 @@ class Beep_Replies {
|
||||
register_rest_route('beep/v1', '/post', [
|
||||
'methods' => 'POST',
|
||||
'callback' => [__CLASS__, 'submit_beep'],
|
||||
'permission_callback' => ['Beep_Likes', 'can_interact'],
|
||||
'permission_callback' => [Beep_Likes::class, 'can_interact'],
|
||||
]);
|
||||
|
||||
register_rest_route('beep/v1', '/reply/(?P<id>\d+)', [
|
||||
'methods' => 'POST',
|
||||
'callback' => [__CLASS__, 'submit_reply'],
|
||||
'permission_callback' => ['Beep_Likes', 'can_interact'],
|
||||
'permission_callback' => [Beep_Likes::class, 'can_interact'],
|
||||
]);
|
||||
register_rest_route('beep/v1', '/thread/(?P<post_id>\d+)', [
|
||||
'methods' => 'GET',
|
||||
'callback' => [__CLASS__, 'get_thread_api'],
|
||||
'permission_callback' => '__return_true',
|
||||
]);
|
||||
register_rest_route('beep/v1', '/delete/(?P<id>\d+)', [
|
||||
|
||||
register_rest_route('beep/v1', '/beep/(?P<id>\d+)', [
|
||||
'methods' => 'DELETE',
|
||||
'callback' => [__CLASS__, 'delete_beep'],
|
||||
'permission_callback' => ['Beep_Likes', 'can_interact'],
|
||||
'permission_callback' => function ($req) {
|
||||
return current_user_can('delete_post', (int) $req['id']);
|
||||
},
|
||||
]);
|
||||
|
||||
register_rest_route('beep/v1', '/reply/(?P<id>\d+)', [
|
||||
'methods' => 'DELETE',
|
||||
'callback' => [__CLASS__, 'delete_reply'],
|
||||
'permission_callback' => ['Beep_Likes', 'can_interact'],
|
||||
'permission_callback' => function () {
|
||||
return current_user_can('moderate_comments');
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
public static function submit_beep($request) {
|
||||
$content = sanitize_textarea_field($request->get_param('content'));
|
||||
if (empty($content)) {
|
||||
return new WP_Error('empty_content', 'Beep cannot be empty.', ['status' => 400]);
|
||||
$content = trim((string) $request->get_param('content'));
|
||||
$length = function_exists('mb_strlen') ? mb_strlen($content) : strlen($content);
|
||||
|
||||
if ($content === '' || $length > BEEP_CHAR_LIMIT) {
|
||||
return new WP_Error('beep_invalid_content',
|
||||
'Content must be 1-' . BEEP_CHAR_LIMIT . ' characters.',
|
||||
['status' => 400]);
|
||||
}
|
||||
|
||||
$post_id = wp_insert_post([
|
||||
'post_type' => 'beep',
|
||||
'post_content' => $content,
|
||||
'post_type' => Beep_CPT::POST_TYPE,
|
||||
'post_status' => 'publish',
|
||||
'post_author' => get_current_user_id(),
|
||||
'post_content' => wp_kses($content, []),
|
||||
'post_title' => wp_trim_words($content, 8, '...'),
|
||||
], true);
|
||||
|
||||
if (is_wp_error($post_id)) {
|
||||
return $post_id;
|
||||
}
|
||||
|
||||
return rest_ensure_response([
|
||||
'id' => $post_id,
|
||||
'html' => beep_render_post(get_post($post_id)),
|
||||
]);
|
||||
if (is_wp_error($post_id)) { return $post_id; }
|
||||
return rest_ensure_response(['id' => $post_id, 'url' => get_permalink($post_id)]);
|
||||
}
|
||||
|
||||
public static function submit_reply($request) {
|
||||
$post_id = (int) $request['id'];
|
||||
$parent = get_post($post_id);
|
||||
if (!$parent || $parent->post_type !== 'beep') {
|
||||
return new WP_Error('invalid_post', 'Invalid beep post.', ['status' => 400]);
|
||||
$post_id = (int) $request['id'];
|
||||
$content = trim((string) $request->get_param('content'));
|
||||
$parent_id = (int) $request->get_param('parent');
|
||||
$length = function_exists('mb_strlen') ? mb_strlen($content) : strlen($content);
|
||||
|
||||
if (get_post_type($post_id) !== Beep_CPT::POST_TYPE) {
|
||||
return new WP_Error('beep_not_a_beep', 'Not a beep.', ['status' => 400]);
|
||||
}
|
||||
$content = sanitize_textarea_field($request->get_param('content'));
|
||||
if (empty($content)) {
|
||||
return new WP_Error('empty_content', 'Reply cannot be empty.', ['status' => 400]);
|
||||
if ($content === '' || $length > BEEP_CHAR_LIMIT) {
|
||||
return new WP_Error('beep_invalid_content',
|
||||
'Reply must be 1-' . BEEP_CHAR_LIMIT . ' characters.',
|
||||
['status' => 400]);
|
||||
}
|
||||
// Validate the parent comment belongs to this post if provided.
|
||||
if ($parent_id > 0) {
|
||||
$parent = get_comment($parent_id);
|
||||
if (!$parent || (int) $parent->comment_post_ID !== $post_id) {
|
||||
return new WP_Error('beep_bad_parent', 'Invalid parent.', ['status' => 400]);
|
||||
}
|
||||
}
|
||||
|
||||
$user = wp_get_current_user();
|
||||
|
||||
$approved = get_option('comment_moderation') ? 0 : 1;
|
||||
if (get_option('comment_previously_approved')) {
|
||||
$prev = get_comments([
|
||||
'user_id' => $user->ID,
|
||||
'status' => 'approve',
|
||||
'count' => true,
|
||||
]);
|
||||
if (!$prev) {
|
||||
$approved = 0;
|
||||
}
|
||||
}
|
||||
|
||||
$comment_id = wp_insert_comment([
|
||||
'comment_post_ID' => $post_id,
|
||||
'comment_content' => $content,
|
||||
'comment_author' => get_current_user_id() ? null : 'Anonymous',
|
||||
'comment_type' => 'comment',
|
||||
'user_id' => get_current_user_id(),
|
||||
'comment_post_ID' => $post_id,
|
||||
'comment_author' => $user->display_name,
|
||||
'comment_author_email' => $user->user_email,
|
||||
'comment_author_url' => '',
|
||||
'comment_content' => wp_kses($content, []),
|
||||
'comment_type' => 'comment',
|
||||
'comment_parent' => $parent_id,
|
||||
'user_id' => $user->ID,
|
||||
'comment_approved' => $approved,
|
||||
]);
|
||||
|
||||
if (!$comment_id) {
|
||||
return new WP_Error('insert_failed', 'Could not save reply.', ['status' => 500]);
|
||||
return new WP_Error('beep_insert_failed', 'Reply could not be saved.', ['status' => 500]);
|
||||
}
|
||||
return rest_ensure_response(['id' => $comment_id]);
|
||||
|
||||
$comment = get_comment($comment_id);
|
||||
$pending = ($comment->comment_approved == 0);
|
||||
|
||||
// Depth of this reply for client-side indenting.
|
||||
$depth = 0;
|
||||
$cur = $comment;
|
||||
while ($cur && (int) $cur->comment_parent > 0 && $depth < 10) {
|
||||
$cur = get_comment((int) $cur->comment_parent);
|
||||
$depth++;
|
||||
}
|
||||
$can_interact = Beep_Likes::can_interact();
|
||||
|
||||
return rest_ensure_response([
|
||||
'id' => $comment_id,
|
||||
'parent' => (int) $comment->comment_parent,
|
||||
'pending' => $pending,
|
||||
'html' => $pending ? '' : beep_render_reply($comment, [], $depth, $post_id, $can_interact),
|
||||
]);
|
||||
}
|
||||
|
||||
public static function delete_beep($request) {
|
||||
$post_id = (int) $request['id'];
|
||||
$post = get_post($post_id);
|
||||
if (!$post || $post->post_type !== 'beep') {
|
||||
return new WP_Error('invalid_post', 'Invalid beep.', ['status' => 400]);
|
||||
if (get_post_type($post_id) !== Beep_CPT::POST_TYPE) {
|
||||
return new WP_Error('beep_not_a_beep', 'Not a beep.', ['status' => 400]);
|
||||
}
|
||||
$deleted = wp_delete_post($post_id, true);
|
||||
if (!$deleted) {
|
||||
return new WP_Error('delete_failed', 'Could not delete.', ['status' => 500]);
|
||||
return new WP_Error('beep_delete_failed', 'Could not delete.', ['status' => 500]);
|
||||
}
|
||||
return rest_ensure_response(['deleted' => true]);
|
||||
}
|
||||
@@ -90,7 +155,7 @@ class Beep_Replies {
|
||||
$comment_id = (int) $request['id'];
|
||||
$deleted = wp_delete_comment($comment_id, true);
|
||||
if (!$deleted) {
|
||||
return new WP_Error('delete_failed', 'Could not delete.', ['status' => 500]);
|
||||
return new WP_Error('beep_delete_failed', 'Could not delete.', ['status' => 500]);
|
||||
}
|
||||
return rest_ensure_response(['deleted' => true]);
|
||||
}
|
||||
@@ -107,110 +172,4 @@ class Beep_Replies {
|
||||
public static function reply_count($post_id) {
|
||||
return (int) get_comments_number($post_id);
|
||||
}
|
||||
|
||||
public static function get_thread($post_id, $depth = 0, $max_depth = 10) {
|
||||
if ($depth > $max_depth) return [];
|
||||
$post = get_post($post_id);
|
||||
if (!$post || $post->post_type !== 'beep') return [];
|
||||
$thread = [self::format_beep_reply($post, $depth)];
|
||||
$replies = get_comments([
|
||||
'post_id' => $post_id, 'status' => 'approve', 'order' => 'ASC',
|
||||
'type' => 'comment', 'number' => 100,
|
||||
]);
|
||||
foreach ($replies as $reply) {
|
||||
if ($depth < $max_depth) {
|
||||
$thread = array_merge($thread, self::get_comment_thread($reply->comment_ID, $depth + 1, $max_depth));
|
||||
}
|
||||
}
|
||||
return $thread;
|
||||
}
|
||||
|
||||
private static function get_comment_thread($comment_id, $depth, $max_depth) {
|
||||
if ($depth > $max_depth) return [];
|
||||
$comment = get_comment($comment_id);
|
||||
if (!$comment) return [];
|
||||
$result = [self::format_comment_reply($comment, $depth)];
|
||||
$children = get_comments([
|
||||
'parent' => $comment_id, 'status' => 'approve', 'order' => 'ASC',
|
||||
'type' => 'comment', 'number' => 100,
|
||||
]);
|
||||
foreach ($children as $child) {
|
||||
$result = array_merge($result, self::get_comment_thread($child->comment_ID, $depth + 1, $max_depth));
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
public static function get_thread_api($request) {
|
||||
$post_id = (int) $request['post_id'];
|
||||
$thread = self::get_thread($post_id);
|
||||
return rest_ensure_response(['thread' => $thread]);
|
||||
}
|
||||
|
||||
public static function format_beep_reply($post, $depth = 0) {
|
||||
$author_id = $post->post_author;
|
||||
$author = get_userdata($author_id);
|
||||
$avatar = get_user_meta($author_id, 'beep_avatar', true);
|
||||
if (!$avatar) {
|
||||
$avatar = 'https://www.gravatar.com/avatar/' . md5(strtolower($author->user_email ?? '')) . '?s=48&d=mp';
|
||||
}
|
||||
return [
|
||||
'id' => $post->ID,
|
||||
'parent_id' => $post->post_parent ?: null,
|
||||
'is_post' => true,
|
||||
'depth' => $depth,
|
||||
'content' => $post->post_content,
|
||||
'time_ago' => self::time_ago(strtotime($post->post_date)),
|
||||
'like_count' => Beep_Likes::get_count($post->ID, 'post'),
|
||||
'reply_count' => self::reply_count($post->ID),
|
||||
'author' => [
|
||||
'id' => $author_id,
|
||||
'name' => $author->display_name ?? 'Unknown',
|
||||
'username' => $author->user_login ?? '',
|
||||
'avatar' => $avatar,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
public static function format_comment_reply($comment, $depth = 0) {
|
||||
$author_id = (int) $comment->user_id;
|
||||
$author = $author_id ? get_userdata($author_id) : null;
|
||||
if ($author_id) {
|
||||
$avatar = get_user_meta($author_id, 'beep_avatar', true);
|
||||
if (!$avatar) {
|
||||
$avatar = 'https://www.gravatar.com/avatar/' . md5(strtolower($author->user_email ?? '')) . '?s=48&d=mp';
|
||||
}
|
||||
$name = $author->display_name ?? 'Unknown';
|
||||
$username = $author->user_login ?? '';
|
||||
} else {
|
||||
$avatar = 'https://www.gravatar.com/avatar/?s=48&d=mp';
|
||||
$name = esc_html($comment->comment_author);
|
||||
$username = '';
|
||||
}
|
||||
return [
|
||||
'id' => $comment->comment_ID,
|
||||
'parent_id' => (int) $comment->comment_parent ?: null,
|
||||
'is_post' => false,
|
||||
'depth' => $depth,
|
||||
'content' => $comment->comment_content,
|
||||
'time_ago' => self::time_ago(strtotime($comment->comment_date)),
|
||||
'like_count' => Beep_Likes::get_count($comment->comment_ID, 'reply'),
|
||||
'reply_count' => 0,
|
||||
'author' => [
|
||||
'id' => $author_id,
|
||||
'name' => $name,
|
||||
'username' => $username,
|
||||
'avatar' => $avatar,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
public static function time_ago($timestamp) {
|
||||
$diff = time() - $timestamp;
|
||||
if ($diff < 0) $diff = 0;
|
||||
if ($diff < 60) return $diff . 's';
|
||||
if ($diff < 3600) return floor($diff / 60) . 'm';
|
||||
if ($diff < 86400) return floor($diff / 3600) . 'h';
|
||||
if ($diff < 604800) return floor($diff / 86400) . 'd';
|
||||
return gmdate('M j', $timestamp);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,6 +86,11 @@ class Beep_Shortcode {
|
||||
$posts = ($sort === 'top') ? self::get_top_beeps($limit) : self::get_latest_beeps($limit);
|
||||
$has_posts = !empty($posts);
|
||||
|
||||
// Show magic login form if not logged in
|
||||
if (!is_user_logged_in()) {
|
||||
return Beep_Magic_Links::render_login_form() . beep_render_public_feed($posts, $atts);
|
||||
}
|
||||
|
||||
ob_start();
|
||||
?>
|
||||
<div class="beep-feed" data-beep-feed>
|
||||
@@ -135,7 +140,19 @@ class Beep_Shortcode {
|
||||
</div>
|
||||
</div>
|
||||
<?php
|
||||
return ob_get_clean() . beep_render_thread_modal() . beep_render_gif_modal();
|
||||
// Output BeepConfig and beep.js inline since wp_enqueue_scripts isn't working
|
||||
$beep_config = sprintf(
|
||||
'<script>var BeepConfig=%s;</script>',
|
||||
json_encode([
|
||||
'restUrl' => esc_url_raw(rest_url('beep/v1/')),
|
||||
'nonce' => wp_create_nonce('wp_rest'),
|
||||
'charLimit' => BEEP_CHAR_LIMIT,
|
||||
'loginUrl' => esc_url_raw(wp_login_url(is_singular() ? get_permalink() : home_url('/'))),
|
||||
'isLoggedIn' => is_user_logged_in(),
|
||||
])
|
||||
);
|
||||
$beep_script = '<script src="' . esc_url(BEEP_URL . 'assets/beep.js?ver=' . BEEP_VERSION) . '" defer></script>';
|
||||
return ob_get_clean() . $beep_config . $beep_script . beep_render_thread_modal() . beep_render_gif_modal();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
// Load WordPress
|
||||
$paths = [
|
||||
"/home/samihost/sami-ahmed.net/wp-load.php",
|
||||
dirname(__FILE__) . "/wp-load.php",
|
||||
dirname(dirname(__FILE__)) . "/wp-load.php"
|
||||
];
|
||||
foreach ($paths as $p) {
|
||||
if (file_exists($p)) {
|
||||
require_once $p;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!defined("ABSPATH")) {
|
||||
die("Could not find wp-load.php");
|
||||
}
|
||||
// Create poll tables
|
||||
global $wpdb;
|
||||
$polls_table = $wpdb->prefix . "beep_polls";
|
||||
$votes_table = $wpdb->prefix . "beep_poll_votes";
|
||||
$charset = $wpdb->get_charset_collate();
|
||||
|
||||
$polls_sql = "CREATE TABLE IF NOT EXISTS $polls_table (
|
||||
id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
beep_id BIGINT(20) UNSIGNED NOT NULL,
|
||||
question TEXT NOT NULL,
|
||||
options TEXT NOT NULL,
|
||||
votes TEXT NOT NULL DEFAULT '[]',
|
||||
total_votes BIGINT(20) UNSIGNED NOT NULL DEFAULT 0,
|
||||
ends_at DATETIME NULL,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (id),
|
||||
KEY beep_id (beep_id)
|
||||
) $charset;";
|
||||
|
||||
$votes_sql = "CREATE TABLE IF NOT EXISTS $votes_table (
|
||||
id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
poll_id BIGINT(20) UNSIGNED NOT NULL,
|
||||
user_id BIGINT(20) UNSIGNED NOT NULL,
|
||||
option_index INT NOT NULL,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY poll_user (poll_id, user_id),
|
||||
KEY poll_id (poll_id),
|
||||
KEY user_id (user_id)
|
||||
) $charset;";
|
||||
|
||||
require_once(ABSPATH . "wp-admin/includes/upgrade.php");
|
||||
$result1 = dbDelta($polls_sql);
|
||||
$result2 = dbDelta($votes_sql);
|
||||
echo "Tables created: $polls_table, $votes_table";
|
||||
echo "
|
||||
Result1: " . print_r($result1, true);
|
||||
echo "
|
||||
Result2: " . print_r($result2, true);
|
||||
?>
|
||||
+49
-13
@@ -7,8 +7,8 @@ if (!defined('ABSPATH')) {
|
||||
* Wordmark (bird + "Beep!") shown at the top of the feed.
|
||||
*/
|
||||
function beep_logo_svg() {
|
||||
$src = esc_url(BEEP_URL . 'assets/beep-wordmark.png');
|
||||
return '<img class="beep-logo" src="' . $src . '" alt="Beep">';
|
||||
$src = esc_url(BEEP_URL . 'assets/beep-wordmark.jpg');
|
||||
return '<span class="beep-logo-wrap"><span class="beep-logo-text">Messages from</span><img class="beep-logo" src="' . $src . '" alt="Beep"></span>';
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -241,6 +241,12 @@ function beep_render_post($post) {
|
||||
<?php echo beep_icon('reply'); ?>
|
||||
<span class="beep-count" data-beep-reply-count><?php echo $reply_count > 0 ? esc_html($reply_count) : ''; ?></span>
|
||||
</button>
|
||||
<button class="beep-action beep-action-retweet"
|
||||
data-beep-retweet-post="<?php echo (int) $post->ID; ?>"
|
||||
type="button"
|
||||
title="Repost">
|
||||
<?php echo beep_icon('retweet'); ?>
|
||||
</button>
|
||||
<button class="beep-action beep-action-like<?php echo $user_liked ? ' is-liked' : ''; ?>"
|
||||
data-beep-like-post="<?php echo (int) $post->ID; ?>"
|
||||
type="button"
|
||||
@@ -249,23 +255,19 @@ function beep_render_post($post) {
|
||||
<span class="beep-icon-filled"><?php echo beep_icon('heart_filled'); ?></span>
|
||||
<span class="beep-count" data-beep-like-count><?php echo $like_count > 0 ? esc_html($like_count) : ''; ?></span>
|
||||
</button>
|
||||
<a href="#" class="beep-action beep-action-conversation"
|
||||
data-beep-thread="<?php echo (int) $post->ID; ?>"
|
||||
title="View conversation">
|
||||
<svg viewBox="0 0 24 24" width="18" height="18" fill="currentColor"><path d="M21 6h-2v9H6v2c0 .55.45 1 1 1h11l4 4V7c0-.55-.45-1-1-1zm-4 6V3c0-.55-.45-1-1-1H3c-.55 0-1 .45-1 1v14l4-4h10c.55 0 1-.45 1-1z"/></svg>
|
||||
</a>
|
||||
<button class="beep-action beep-action-bookmark"
|
||||
data-beep-bookmark-post="<?php echo (int) $post->ID; ?>"
|
||||
type="button"
|
||||
title="Bookmark">
|
||||
<?php echo beep_icon('bookmark'); ?>
|
||||
</button>
|
||||
<button class="beep-action beep-action-share"
|
||||
data-beep-share="<?php echo esc_url(get_permalink($post->ID)); ?>"
|
||||
type="button"
|
||||
title="Copy link">
|
||||
title="Share">
|
||||
<?php echo beep_icon('share'); ?>
|
||||
</button>
|
||||
</div>
|
||||
<a href="#" class="beep-action beep-action-quote"
|
||||
data-beep-quote="<?php echo (int) $post->ID; ?>"
|
||||
title="Quote">
|
||||
<svg viewBox="0 0 24 24" width="18" height="18" fill="currentColor"><path d="M8 5H6v2H4v12h2v-2h2v2h8V9h-2V7h-2v2H8V5zm10 0v2h2v12h-2v-2h-2v2H8V9h2v2h2V7h2V5h2z"/></svg>
|
||||
</a>
|
||||
<?php if ($can_delete) : ?>
|
||||
<div class="beep-delete-row">
|
||||
<button class="beep-delete-link"
|
||||
@@ -518,3 +520,37 @@ function beep_render_gif_modal() {
|
||||
'<div class="beep-gif-grid" data-beep-gif-grid></div>' .
|
||||
'</div>';
|
||||
}
|
||||
|
||||
|
||||
function beep_render_public_feed($posts, $atts) {
|
||||
ob_start();
|
||||
?>
|
||||
<div class="beep-feed" data-beep-feed>
|
||||
<div class="beep-feed-header">
|
||||
<?php if (!empty($atts['show_wordmark'])): ?>
|
||||
<?php echo beep_logo_svg(); ?>
|
||||
<?php else: ?>
|
||||
<span class="beep-feed-header-text"><?php echo esc_html($atts['header']); ?></span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<div class="beep-public-notice">
|
||||
<svg viewBox="0 0 24 24" width="16" height="16" fill="currentColor">
|
||||
<path d="M12 2C6.477 2 2 6.477 2 12s4.477 10 10 10 10-4.477 10-10S17.523 2 12 2zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z"/>
|
||||
</svg>
|
||||
<span><a href="<?php echo wp_login_url(); ?>">Sign in</a> to post</span>
|
||||
</div>
|
||||
|
||||
<?php if (!empty($posts)): ?>
|
||||
<div class="beep-posts">
|
||||
<?php foreach ($posts as $post): ?>
|
||||
<?php echo beep_render_post($post); ?>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div class="beep-empty">No beeps yet.</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php
|
||||
return ob_get_clean();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user