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:
Krystie
2026-05-20 15:12:41 -07:00
parent dfd621e350
commit b882092f5d
11 changed files with 1504 additions and 1495 deletions
+737 -1261
View File
File diff suppressed because it is too large Load Diff
+73
View File
@@ -970,3 +970,76 @@
btn.disabled = false;
});
}
// Magic Login Form Handler
document.addEventListener('DOMContentLoaded', function() {
var magicForm = document.querySelector('[data-beep-magic-form]');
if (magicForm) {
magicForm.addEventListener('submit', function(e) {
e.preventDefault();
var emailInput = magicForm.querySelector('[name="email"]');
var submitBtn = magicForm.querySelector('[data-beep-magic-submit]');
var btnText = submitBtn.querySelector('.beep-magic-btn-text');
var loading = submitBtn.querySelector('.beep-magic-loading');
var successMsg = document.querySelector('[data-beep-magic-success]');
var errorMsg = document.querySelector('[data-beep-magic-error]');
var email = emailInput.value;
// Hide previous messages
if (successMsg) successMsg.hidden = true;
if (errorMsg) errorMsg.hidden = true;
// Show loading
btnText.hidden = true;
loading.hidden = false;
submitBtn.disabled = true;
// Get REST URL and nonce from page
var restUrl = typeof BeepConfig !== 'undefined' ? BeepConfig.restUrl : '/wp-json/';
var nonce = typeof BeepConfig !== 'undefined' ? BeepConfig.nonce : '';
fetch(restUrl + 'magic-link', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-WP-Nonce': nonce
},
body: JSON.stringify({ email: email })
})
.then(function(response) {
return response.json();
})
.then(function(data) {
if (data.success || (data.message && data.message.includes('sent'))) {
// Success
if (successMsg) {
successMsg.hidden = false;
magicForm.hidden = true;
}
} else {
// Error
var errorText = data.message || 'Something went wrong. Please try again.';
if (errorMsg) {
errorMsg.textContent = errorText;
errorMsg.hidden = false;
}
}
})
.catch(function(err) {
console.error('Magic login error:', err);
if (errorMsg) {
errorMsg.textContent = 'Something went wrong. Please try again.';
errorMsg.hidden = false;
}
})
.finally(function() {
btnText.hidden = false;
loading.hidden = true;
submitBtn.disabled = false;
});
});
}
});
+25 -22
View File
@@ -1,8 +1,8 @@
<?php
/**
* Plugin Name: Beep
* Description: A Twitter-style microblog for WordPress, with likes, replies, and Gravatars. Use the [beep_feed] shortcode on any page.
* Version: 1.4.1
* Description: A Twitter-style microblog for WordPress, with likes, replies, polls, and Gravatars. Use the [beep_feed] shortcode on any page.
* Version: 1.5.0
* Requires at least: 6.0
* Requires PHP: 7.4
* Author: Sami Ahmed
@@ -10,29 +10,32 @@
* Text Domain: beep
*/
if (!defined('ABSPATH')) {
if (!defined("ABSPATH")) {
exit;
}
define('BEEP_VERSION', '1.4.1');
define('BEEP_FILE', __FILE__);
define('BEEP_DIR', plugin_dir_path(__FILE__));
define('BEEP_URL', plugin_dir_url(__FILE__));
define('BEEP_LIKES_TABLE', 'beep_likes');
define('BEEP_CHAR_LIMIT', 280);
define("BEEP_VERSION", "1.5.0");
define("BEEP_FILE", __FILE__);
define("BEEP_DIR", plugin_dir_path(__FILE__));
define("BEEP_URL", plugin_dir_url(__FILE__));
define("BEEP_LIKES_TABLE", "beep_likes");
define("BEEP_CHAR_LIMIT", 280);
require_once BEEP_DIR . 'includes/helpers.php';
require_once BEEP_DIR . 'includes/class-cpt.php';
require_once BEEP_DIR . 'includes/class-likes.php';
require_once BEEP_DIR . 'includes/class-replies.php';
require_once BEEP_DIR . 'includes/class-moderation.php';
require_once BEEP_DIR . 'includes/class-media.php';
require_once BEEP_DIR . 'includes/class-quotes.php';
require_once BEEP_DIR . 'includes/class-polls.php';
require_once BEEP_DIR . 'includes/class-shortcode.php';
require_once BEEP_DIR . 'includes/class-beep.php';
require_once BEEP_DIR . "includes/helpers.php";
require_once BEEP_DIR . "includes/class-cpt.php";
require_once BEEP_DIR . "includes/class-likes.php";
require_once BEEP_DIR . "includes/class-replies.php";
require_once BEEP_DIR . "includes/class-moderation.php";
require_once BEEP_DIR . "includes/class-media.php";
require_once BEEP_DIR . "includes/class-quotes.php";
require_once BEEP_DIR . "includes/class-polls.php";
require_once BEEP_DIR . "includes/class-follows.php";
require_once BEEP_DIR . "includes/class-shortcode.php";
require_once BEEP_DIR . "includes/class-magic-links.php";
require_once BEEP_DIR . "includes/class-beep.php";
register_activation_hook(__FILE__, ['Beep_Plugin', 'activate']);
register_deactivation_hook(__FILE__, ['Beep_Plugin', 'deactivate']);
register_activation_hook(__FILE__, ["Beep_Plugin", "activate"]);
register_deactivation_hook(__FILE__, ["Beep_Plugin", "deactivate"]);
add_action('plugins_loaded', ['Beep_Plugin', 'init']);
add_action("plugins_loaded", ["Beep_Plugin", "init"]);
Beep_Magic_Links::init();
+2 -54
View File
@@ -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
+207
View File
@@ -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();
});
+234
View File
@@ -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
View File
@@ -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);
}
}
+18 -1
View File
@@ -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();
}
/**
+56
View File
@@ -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
View File
@@ -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();
}