Skip to content

Commit d84a029

Browse files
release: fixes
- Fixed creation hashtags on Bluesky network - Fixed issue with Google Business Profile posting failure when locations are connected from multiple Google account - Added automatic cleanup for sharing logs to prevent database slowdowns - Improved status showing up correctly in dashboard when using Start/Stop sharing buttons - Updated dependencies
2 parents b7da864 + 6495d29 commit d84a029

15 files changed

Lines changed: 397 additions & 49 deletions

.distignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,4 +32,5 @@ vue
3232
webpack.config.js
3333
docker-compose.yml
3434
phpstan.neon
35-
phpstan-baseline.neon
35+
phpstan-baseline.neon
36+
AGENTS.md

AGENTS.md

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
# Agent Workflow
2+
3+
## Project Overview
4+
5+
**Revive Old Posts (Revive Social)** — a WordPress plugin that automatically shares WordPress posts to social networks (Facebook, X/Twitter, LinkedIn, Instagram, Telegram, Mastodon, BlueSky, VK, Pinterest, TikTok, etc.) with scheduling and automation.
6+
7+
- **Text Domain:** `tweet-old-post`
8+
- **Main Plugin File:** `tweet-old-post.php`
9+
- **Pro Companion Plugin:** `tweet-old-post-pro` (separate repo)
10+
11+
## Build & Development Commands
12+
13+
```bash
14+
# Install dependencies
15+
npm ci
16+
composer install
17+
18+
# Build assets (Vue dashboard + React sharing panel)
19+
npm run build # Production webpack build (Vue)
20+
npm run sharing # Production wp-scripts build (React)
21+
22+
# Development with watch
23+
npm run dev # Webpack watch mode (Vue)
24+
npm run sharing-dev # wp-scripts dev mode (React)
25+
26+
# Linting
27+
composer run lint # PHPCS (WordPress-Core standard)
28+
composer run format # Auto-fix PHP code style
29+
npm run lint # ESLint for Vue + React files
30+
npm run format # ESLint auto-fix
31+
32+
# Testing
33+
composer run test # PHPUnit (all suites)
34+
./vendor/bin/phpunit tests/test-content.php # Single test file
35+
composer run phpstan # Static analysis (level 6)
36+
37+
# E2E Testing (requires wp-env)
38+
npm run wp-env start # Start WordPress environment
39+
npm run test:e2e:playwright # Run Playwright E2E tests
40+
npm run test:e2e:playwright:ui # E2E with Playwright UI mode
41+
42+
# Distribution
43+
npm run dist # Create distribution ZIP archive
44+
```
45+
46+
## Architecture
47+
48+
### Entry Point & Autoloading
49+
50+
`tweet-old-post.php` defines constants (prefixed `ROP_`), registers activation/deactivation hooks, sets up a custom autoloader (`class-rop-autoloader.php`), and calls `run_rop()` which instantiates the `Rop` core class.
51+
52+
The autoloader scans `includes/` recursively, matching classes by the `Rop` namespace prefix. Class files follow the convention `class-{slug-case-name}.php`.
53+
54+
### Core Class Hierarchy
55+
56+
- **`Rop`** (`includes/class-rop.php`) — Core plugin class. Loads dependencies, sets up i18n, defines admin hooks.
57+
- **`Rop_Loader`** (`includes/class-rop-loader.php`) — Central hook registration system. Actions/filters are queued then bulk-registered.
58+
- **`Rop_Admin`** (`includes/admin/class-rop-admin.php`) — Admin UI, script/style enqueuing, menu registration. ~2,000 lines.
59+
- **`Rop_Rest_Api`** (`includes/admin/class-rop-rest-api.php`) — REST endpoints at `tweet-old-post/v8/api` and `tweet-old-post/v8/share/{id}`. Requires `manage_options` capability. ~1,700 lines.
60+
61+
### Service Layer (Social Networks)
62+
63+
`includes/admin/services/` — Each social network has a service class extending `Rop_Services_Abstract` (Strategy pattern):
64+
- Key methods: `get_service_credentials()`, `login()`, `publish()`, `get_account()`
65+
- Services: Twitter, Facebook, LinkedIn, Mastodon, BlueSky, Telegram, VK, Pinterest, Tumblr, GMB, Webhook
66+
67+
### Models (Data Layer)
68+
69+
`includes/admin/models/` — Settings, services, queue, scheduler, post format, post selector, URL shorteners. Models store data in WordPress options.
70+
71+
### Helpers
72+
73+
`includes/admin/helpers/` — Content manipulation, post formatting, cron scheduling, DB migrations, logging, custom API clients (Telegram, BlueSky).
74+
75+
### URL Shorteners
76+
77+
`includes/admin/shortners/` — Each shortener extends `Rop_Url_Shortner_Abstract` (Bitly, Firebase, Rebrandly, is.gd, ow.ly, rviv.ly).
78+
79+
### Frontend (Two Systems)
80+
81+
1. **Vue 2 Dashboard** (legacy) — Built via `webpack.config.js`. Entry points in `vue/src/` → output to `assets/js/build/`. Uses Vuex for state, vue-resource for HTTP.
82+
2. **React Components** (new) — Built via `webpack.sharing.config.js` using `@wordpress/scripts`. Source in `src/` → output to `assets/js/react/build/`. Used for instant/manual sharing in the block editor.
83+
84+
### Cron System
85+
86+
`cron-system/` — Alternative remote cron implementation (`RopCronSystem\Rop_Cron_Core`). Activated when `ROP_CRON_ALTERNATIVE` is true (controlled by `rop_use_remote_cron` option).
87+
88+
### External Auth
89+
90+
Social network authentication goes through `ROP_AUTH_APP_URL` (`https://app.revive.social`) with per-service paths (`/fb_auth`, `/tw_auth`, `/li_auth`, etc.).
91+
92+
## Coding Standards
93+
94+
- **PHP:** WordPress-Core via PHPCS (`phpcs.xml`) with many naming convention rules relaxed — camelCase variables/methods are allowed throughout the codebase
95+
- **JS (Vue):** `plugin:vue/recommended` with babel-eslint parser
96+
- **JS (React):** `@wordpress/eslint-plugin/recommended` with text domain enforced as `tweet-old-post`
97+
- **Static Analysis:** PHPStan level 6 with WordPress extension and baseline (`phpstan-baseline.neon`)
98+
99+
## Testing Structure
100+
101+
PHPUnit test suites are defined in `phpunit.xml` with individual files in `tests/`:
102+
- `test-plugin.php`, `test-accounts.php`, `test-content.php`, `test-logger.php`, `test-post-format.php`, `test-queue.php`, `test-scheduler.php`, `test-selector.php`
103+
104+
E2E tests use Playwright with `@wordpress/e2e-test-utils-playwright`. Specs live in `tests/e2e/specs/`. Config at `tests/e2e/playwright.config.js`.
105+
106+
PHPUnit bootstrap (`tests/bootstrap.php`) requires WordPress test suite via `WP_TESTS_DIR` env var.

composer.lock

Lines changed: 8 additions & 8 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

includes/admin/abstract/class-rop-model-abstract.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ protected function set( $key, $value = '', $refresh = false ) {
9292

9393
$this->data[ $key ] = apply_filters( 'rop_set_key_' . $key, $value );
9494

95-
return update_option( $this->namespace, $this->data );
95+
return update_option( $this->namespace, $this->data, false );
9696
}
9797

9898

includes/admin/class-rop-admin.php

Lines changed: 40 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1852,6 +1852,12 @@ public function get_survey_metadata() {
18521852
* @return array
18531853
*/
18541854
public function rop_upgrade_to_pro_plugin_action( $actions, $plugin_file ) {
1855+
1856+
$is_black_friday = apply_filters( 'themeisle_sdk_is_black_friday_sale', false );
1857+
if ( $is_black_friday ) {
1858+
return $actions;
1859+
}
1860+
18551861
$global_settings = new \Rop_Global_Settings();
18561862
$actions['settings'] = '<a href="' . admin_url( 'admin.php?page=TweetOldPost' ) . '">' . __( 'Settings', 'tweet-old-post' ) . '</a>';
18571863
if ( $global_settings->license_type() < 1 ) {
@@ -1908,32 +1914,54 @@ public function get_languages() {
19081914
public static function add_black_friday_data( $configs ) {
19091915
$config = $configs['default'];
19101916

1911-
// translators: %1$s - HTML tag, %2$s - discount, %3$s - HTML tag, %4$s - product name.
1912-
$message_template = __( 'Our biggest sale of the year: %1$sup to %2$s OFF%3$s on %4$s. Don\'t miss this limited-time offer.', 'tweet-old-post' );
1913-
$product_label = __( 'Revive Social', 'tweet-old-post' );
1914-
$discount = '50%';
1917+
$message = __( 'Custom schedules, multiple accounts, analytics. Automate your social sharing properly. Exclusively for existing Revive Social users.', 'tweet-old-post' );
1918+
$cta_label = __( 'Get Revive Social Pro', 'tweet-old-post' );
19151919

19161920
$plan = apply_filters( 'product_rop_license_plan', 0 );
19171921
$license = apply_filters( 'product_rop_license_key', false );
1918-
$is_pro = 0 < $plan;
1922+
$status = apply_filters( 'product_rop_license_status', false );
1923+
$is_pro = 'valid' === $status;
1924+
$is_expired = 'expired' === $status || 'active-expired' === $status;
1925+
1926+
$pro_product_slug = defined( 'ROP_PRO_BASEFILE' ) ? basename( dirname( ROP_PRO_BASEFILE ) ) : '';
1927+
1928+
if ( $is_pro || $is_expired ) {
1929+
$config['plugin_meta_targets'] = array( $pro_product_slug );
1930+
}
19191931

19201932
if ( $is_pro ) {
1921-
// translators: %1$s - HTML tag, %2$s - discount, %3$s - HTML tag, %4$s - product name.
1922-
$message_template = __( 'Get %1$sup to %2$s off%3$s when you upgrade your %4$s plan or renew early.', 'tweet-old-post' );
1923-
$product_label = __( 'Revive Social Pro', 'tweet-old-post' );
1924-
$discount = '20%';
1933+
// translators: %s is the discount percentage.
1934+
$config['plugin_meta_message'] = sprintf( __( 'Black Friday Sale - up to %s off', 'tweet-old-post' ), '30%' );
1935+
// translators: %1$s - discount, %2$s - discount.
1936+
$message = sprintf( __( 'Upgrade your Revive Social Pro plan: %1$s off this week. Already on the plan you need? Renew early and save up to %2$s.', 'tweet-old-post' ), '30%', '20%' );
1937+
$cta_label = __( 'See your options', 'tweet-old-post' );
1938+
} elseif ( $is_expired ) {
1939+
// translators: %s is the discount percentage.
1940+
$config['plugin_meta_message'] = sprintf( __( 'Black Friday Sale - %s off', 'tweet-old-post' ), '50%' );
1941+
// translators: %s - discount.
1942+
$config['upgrade_menu_text'] = sprintf( __( 'BF Sale - %s off', 'tweet-old-post' ), '50%' );
1943+
$message = __( 'Your Revive Social Pro features are still here, just locked. Renew at a reduced rate this week.', 'tweet-old-post' );
1944+
$cta_label = __( 'Reactivate now', 'tweet-old-post' );
1945+
} else {
1946+
// translators: %s is the discount percentage.
1947+
$config['plugin_meta_message'] = sprintf( __( 'Black Friday Sale - %s off', 'tweet-old-post' ), '50%' );
1948+
// translators: %s - discount.
1949+
$config['title'] = sprintf( __( 'Revive Social Pro: %s off this week', 'tweet-old-post' ), '60%' );
1950+
// translators: %s - discount.
1951+
$config['upgrade_menu_text'] = sprintf( __( 'BF Sale - %s off', 'tweet-old-post' ), '60%' );
19251952
}
19261953

1927-
$product_label = sprintf( '<strong>%s</strong>', $product_label );
19281954
$url_params = array(
19291955
'utm_term' => $is_pro ? 'plan-' . $plan : 'free',
19301956
'lkey' => ! empty( $license ) ? $license : false,
1957+
'expired' => $is_expired ? '1' : false,
19311958
);
19321959

1933-
$config['message'] = sprintf( $message_template, '<strong>', $discount, '</strong>', $product_label );
1960+
$config['message'] = $message;
1961+
$config['cta_label'] = $cta_label;
19341962
$config['sale_url'] = add_query_arg(
19351963
$url_params,
1936-
tsdk_translate_link( tsdk_utmify( 'https://themeisle.com/rs-bf', 'bfcm', 'revive' ) )
1964+
tsdk_translate_link( tsdk_utmify( 'https://themeisle.link/rs-bf', 'bfcm', 'revive' ) )
19371965
);
19381966

19391967
$configs[ ROP_PRODUCT_SLUG ] = $config;

includes/admin/class-rop-logger.php

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,14 @@ class Rop_Logger {
2929
*/
3030
private $stream;
3131

32+
/**
33+
* Holds the log namespace.
34+
*
35+
* @access private
36+
* @var string $log_namespace Defaults 'rop_logs'.
37+
*/
38+
private $log_namespace = 'rop_logs';
39+
3240
/**
3341
* Rop_Logger constructor.
3442
* Instantiate the Logger with specified formatter and stream.
@@ -38,7 +46,7 @@ class Rop_Logger {
3846
*/
3947
public function __construct() {
4048

41-
$this->stream = new Rop_Log_Handler( 'rop_logs' );
49+
$this->stream = new Rop_Log_Handler( $this->log_namespace );
4250

4351
}
4452

@@ -230,4 +238,23 @@ public function is_status_error_necessary( $logs = array() ) {
230238

231239
}
232240

241+
/**
242+
* Utility method to cleanup authenticated services.
243+
*
244+
* @access public
245+
*
246+
* @param int $cleanup_log_count Cleanup the log count.
247+
* @return array<string, mixed>
248+
*/
249+
public function cleanup_logs( $cleanup_log_count = 1000 ) {
250+
$logs = $this->stream->get_logs();
251+
252+
if ( count( $logs ) > $cleanup_log_count ) {
253+
$logs = array_slice( $logs, 0, $cleanup_log_count );
254+
255+
update_option( $this->log_namespace, array_reverse( $logs ), false );
256+
}
257+
258+
return $logs;
259+
}
233260
}

includes/admin/class-rop-rest-api.php

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1642,6 +1642,20 @@ private function add_account_bluesky( $data ) {
16421642
return $this->response->to_array();
16431643
}
16441644

1645+
/**
1646+
* API method called to cleanup logs.
1647+
*
1648+
* @access private
1649+
* @return array<string, mixed>
1650+
*/
1651+
private function cleanup_logs() {
1652+
$model = new Rop_Logger();
1653+
$this->response->set_code( '200' )
1654+
->set_data( $model->cleanup_logs() );
1655+
1656+
return $this->response->to_array();
1657+
}
1658+
16451659
/**
16461660
* Share API method.
16471661
*

includes/admin/helpers/class-rop-bluesky-api.php

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -290,12 +290,19 @@ public function create_post( $did, $post, $post_type, $hashtags, $access_token =
290290

291291
$now = gmdate( 'Y-m-d\TH:i:s\Z' );
292292

293+
$text = $post['content'] . $hashtags;
294+
293295
$record = array(
294296
'$type' => 'app.bsky.feed.post',
295-
'text' => $post['content'] . $hashtags,
297+
'text' => $text,
296298
'createdAt' => $now,
297299
);
298300

301+
$facets = $this->generate_facets( $text );
302+
if ( ! empty( $facets ) ) {
303+
$record['facets'] = $facets;
304+
}
305+
299306
if ( $post_type === 'link' && isset( $post['post_url'] ) && ! empty( $post['post_url'] ) ) {
300307
$record['embed'] = array(
301308
'$type' => 'app.bsky.embed.external',
@@ -369,6 +376,41 @@ public function create_post( $did, $post, $post_type, $hashtags, $access_token =
369376
}
370377
}
371378

379+
/**
380+
* Generate facets for hashtags.
381+
*
382+
* @param string $text The text to parse.
383+
*
384+
* @return mixed|array
385+
*/
386+
private function generate_facets( $text ) {
387+
$facets = array();
388+
preg_match_all( '/#\S+/', $text, $matches, PREG_OFFSET_CAPTURE );
389+
390+
if ( ! empty( $matches[0] ) ) {
391+
foreach ( $matches[0] as $match ) {
392+
$hashtag = $match[0];
393+
$start = $match[1];
394+
$end = $start + strlen( $hashtag );
395+
396+
$facets[] = array(
397+
'index' => array(
398+
'byteStart' => $start,
399+
'byteEnd' => $end,
400+
),
401+
'features' => array(
402+
array(
403+
'$type' => 'app.bsky.richtext.facet#tag',
404+
'tag' => ltrim( $hashtag, '#' ),
405+
),
406+
),
407+
);
408+
}
409+
}
410+
411+
return $facets;
412+
}
413+
372414
/**
373415
* Fetch Website Card embeds.
374416
*

includes/admin/models/class-rop-services-model.php

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,6 @@ class Rop_Services_Model extends Rop_Model_Abstract {
3737
*/
3838
private $accounts_namespace = 'active_accounts';
3939

40-
4140
/**
4241
* Utility method to clear authenticated services.
4342
*
@@ -519,5 +518,4 @@ public function find_account( $account_id ) {
519518

520519
return false;
521520
}
522-
523521
}

0 commit comments

Comments
 (0)