Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -56,11 +56,12 @@ Copies a HelpScout mailbox's conversations into a FreeScout mailbox, so a team's
- FreeScout's queue worker must be running.
- Conversation numbers: under Manage » Settings » General, set **Conversation Number** to **Custom…** (`APP_CUSTOM_NUMBER=true`), or FreeScout shows and searches its internal IDs instead. Before the first import, set the **Next Conversation #** that appears well above HelpScout's numbers, like 2,000,000: HelpScout keeps numbering the mailboxes that haven't moved yet, and conversations whose number FreeScout has given away already keep a number of FreeScout's. The import page warns until both are set, and doesn't start an import before the Next Conversation # is. Imports leave that setting alone.
- Teams: install the Teams module, and create the teams, before importing mailboxes with conversations assigned to teams. Tick the mailboxes each team works in on its page: a team only has a folder, and can be assigned again, in those, even when the import assigns it a conversation elsewhere.
- **WordPress.org:** WordPress.org keeps a copy of conversations, which the plugin directory reads. It points at HelpScout until a mailbox switches. To switch one, in this order:
- **WordPress.org:** WordPress.org keeps a copy of conversations, which the plugin directory links to, and replies to uploads in. It points at HelpScout until a mailbox switches. To switch one, in this order:
1. In HelpScout, stop the mailbox's events going to `api.wordpress.org/dotorg/helpscout/webhook.php`, its webhook under **Manage » Apps**: scope it to the mailboxes still in HelpScout, or turn it off once none is left. While it runs for the mailbox, anything done there writes HelpScout's copy of a conversation back, and counts in HelpScout's stats. It leaves alone the HelpScout conversations whose copies FreeScout's replaced, as each copy names the one it replaced, but not the others.
2. Run the last import of the mailbox.
3. Together with moving the mailbox's email to FreeScout, press **Point WordPress.org at FreeScout** for it, under **WordPress.org** on the import page. That sends WordPress.org which FreeScout conversation each HelpScout one was imported as, 250 at a time, on the queue (`api.wordpress.org/dotorg/freescout/replace-copies.php`), without importing anything again. The copies of conversations in FreeScout's Deleted folder, or marked spam, are deleted instead. The import page shows how many were sent, and when it's done. If WordPress.org doesn't take a batch, it says why, and **Send again** sends them all again; what was sent already doesn't change.
4. After the cutover, import the mailbox again, for what HelpScout got in between. Every import that finishes after the switch sends all of the mailbox's imported conversations again.
4. For the Plugins mailbox, define `FREESCOUT_PLUGINS_MAILBOX_ID` and `FREESCOUT_API_KEY`, FreeScout's global API key, on WordPress.org, so the plugin directory's links, searches, new conversations, and queue stats go to FreeScout, and upload confirmations are replied there. The plugin directory writes as FreeScout user 81, connected to wordpressdotorg, which needs access to the Plugins mailbox. The queue stats need the Reports module. Set the Plugins mailbox's **Status after replying** to what HelpScout's was, as authors see their review waiting on them only while its conversation is closed.
5. After the cutover, import the mailbox again, for what HelpScout got in between. Every import that finishes after the switch sends all of the mailbox's imported conversations again.

Until a mailbox switches, `WPOrgWebhooks` sends imported conversations' events for stats, but not the conversations, so WordPress.org goes on pointing at HelpScout; after, it sends them, naming the HelpScout conversation each replaces.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,14 @@
* @package WordPressdotorg\Plugin_Directory\Admin
*/
class Status_Transitions {
/**
* The namespace a rejected plugin's slug is wrapped in.
*
* The trailing digits are the suffix `wp_update_post()` adds when the wrapped slug
* is itself taken.
*/
const REJECTED_SLUG_REGEX = '/^rejected-(.+)-rejected(-\d+)?$/i';

/**
* Fetch the instance of the Status_Transitions class.
*/
Expand Down Expand Up @@ -364,7 +372,7 @@ public function rejected( $post_id, $post ) {
*/
public function restore_rejected_plugin( $post ) {
$slug = $post->post_name;
$slug = preg_replace( '!^rejected-(.+)-rejected$!i', '$1', $slug );
$slug = preg_replace( self::REJECTED_SLUG_REGEX, '$1', $slug );

// Change slug back to 'plugin-name'.
wp_update_post( array(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use WordPressdotorg\Plugin_Directory\Template;
use WordPressdotorg\Plugin_Directory\Tools;
use WordPressdotorg\Plugin_Directory\Admin\Tools\Related_Submissions;
use WordPressdotorg\Plugin_Directory\Tools\Helpdesk;

/**
* The Author Card admin metabox.
Expand Down Expand Up @@ -125,9 +126,11 @@ public static function display( $post_or_user_id = '' ) {
esc_attr__( 'Click to search Pluginrepo P2 for mentions of this author', 'wporg-plugins' )
),
sprintf(
'<a href="https://secure.helpscout.net/search/?query=mailbox:Plugins%%20%s" title="%s">HS</a>',
urlencode( esc_attr( $author->user_nicename ) ),
esc_attr__( 'Click to search Help Scout for mentions of this author', 'wporg-plugins' )
'<a href="%s" title="%s">%s</a>',
esc_url( Helpdesk::search_url( $author->user_nicename ) ),
/* translators: %s: Helpdesk name, like FreeScout. */
esc_attr( sprintf( __( 'Click to search %s for mentions of this author', 'wporg-plugins' ), Helpdesk::name() ) ),
esc_html( Helpdesk::name() )
),
);
vprintf( '<span class="profile-sp-link">[ %s | %s ]</span>', array_map( 'wp_kses_post', $author_links ) );
Expand All @@ -143,9 +146,11 @@ public static function display( $post_or_user_id = '' ) {
<?php
$author_email_links = array(
sprintf(
'<a href="https://secure.helpscout.net/search/?query=mailbox:Plugins%%20%s" title="%s">HS</a>',
urlencode( $author->user_email ),
esc_attr__( 'Click to search Help Scout for emails sent to/from this email address', 'wporg-plugins' )
'<a href="%s" title="%s">%s</a>',
esc_url( Helpdesk::search_url( $author->user_email ) ),
/* translators: %s: Helpdesk name, like FreeScout. */
esc_attr( sprintf( __( 'Click to search %s for emails sent to/from this email address', 'wporg-plugins' ), Helpdesk::name() ) ),
esc_html( Helpdesk::name() )
),
);
vprintf( '<span class="profile-sp-link">[ %s ]</span>', array_map( 'wp_kses_post', $author_email_links ) );
Expand Down Expand Up @@ -527,9 +532,11 @@ protected static function display_plugin_links( $all_plugins, $author_plugins, $
esc_attr__( 'Click to search Plugin Team P2 for mentions of this plugin', 'wporg-plugins' )
),
sprintf(
'<a href="https://secure.helpscout.net/search/?query=mailbox:Plugins%%20%s" title="%s">HS</a>',
rawurlencode( esc_attr( $plugin_name ) ),
esc_attr__( 'Click to search Help Scout for mentions of this plugin', 'wporg-plugins' )
'<a href="%s" title="%s">%s</a>',
esc_url( Helpdesk::search_url( $plugin_name ) ),
/* translators: %s: Helpdesk name, like FreeScout. */
esc_attr( sprintf( __( 'Click to search %s for mentions of this plugin', 'wporg-plugins' ), Helpdesk::name() ) ),
esc_html( Helpdesk::name() )
),
] );

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

use WordPressdotorg\Plugin_Directory\Template;
use WordPressdotorg\Plugin_Directory\Tools;
use WordPressdotorg\Plugin_Directory\Tools\Helpdesk;
use WordPressdotorg\Plugin_Directory\Jobs\Plugin_Import;
use WordPressdotorg\Plugin_Directory\Jobs\Plugin_i18n_Import;

Expand Down Expand Up @@ -420,7 +421,7 @@ public static function display() {
}, $committers );

$cc_emails = wp_list_pluck( $committers, 'user_email' );
$cc_emails = implode( ',', array_diff( $cc_emails, array( $author->user_email ) ) );
$cc_emails = array_values( array_diff( $cc_emails, array( $author->user_email ) ) );

if ( 'new' === $post->post_status || 'pending' === $post->post_status ) {
/* translators: %s: Plugin Title */
Expand Down Expand Up @@ -448,8 +449,7 @@ public static function display() {
$subject = sprintf( __( '[WordPress Plugin Directory] Notice: %s', 'wporg-plugins' ), $post->post_title );
}

// HelpScout requires urlencode() because it wants spaces as + signs.
$contact_author = 'https://secure.helpscout.net/mailbox/ad3e85554c5bd064/new-ticket/?name=' . $author->display_name . '&email=' . urlencode( $author->user_email ) . '&cc=' . urlencode( $cc_emails ) . '&subject=' . urlencode( $subject );
$contact_author = Helpdesk::new_conversation_url( $author, $cc_emails, $subject );
?>
<a id="contact-author" class="button button-primary" href="<?php echo esc_url( $contact_author ); ?>">Contact plugin committer(s)</a>
<?php
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
<?php
namespace WordPressdotorg\Plugin_Directory\Admin\Tools;
use WordPressdotorg\Plugin_Directory\Template;
use WordPressdotorg\Plugin_Directory\Clients\FreeScout;
use WordPressdotorg\Plugin_Directory\Clients\HelpScout;
use WordPressdotorg\Plugin_Directory\Tools\Helpdesk;

/**
* All functionality related to Stats_Report Tool.
Expand Down Expand Up @@ -80,14 +82,14 @@ public function admin_page_access_denied() {
* @type array $in_queue_pending_why The number of pending plugins currently in the queue, broken down by whom we're waiting on.
* @type int $in_queue_from_time_window The number of plugins currently in the queue submitted during the specified time window.
* @type int $in_queue_old The number of plugins currently in the queue that are older than "recently".
* @type int $helpscout_queue_total_conversations The number of ongoing Help Scout conversations.
* @type int $helpscout_queue_new_conversations The number of new Help Scout conversations.
* @type int $helpscout_queue_customers The number of unique Plugin authors contacted.
* @type int $helpscout_queue_conversations_per_day The number of Help Scout conversations per day.
* @type int $helpscout_queue_busiest_day The busiest day in the Help Scout queue.
* @type int $helpscout_queue_messages_received The number of emails received in HelpScout.
* @type int $helpscout_queue_replies_sent The number of replies sent to emails.
* @type int $helpscout_queue_emails_created The number of new outgoing conversations created.
* @type int $email_queue_total_conversations The number of ongoing conversations in the helpdesk.
* @type int $email_queue_new_conversations The number of new conversations in the helpdesk.
* @type int $email_queue_customers The number of unique Plugin authors contacted.
* @type int $email_queue_conversations_per_day The number of conversations per day.
* @type string $email_queue_busiest_day The busiest day in the helpdesk's queue.
* @type int $email_queue_messages_received The number of emails received.
* @type int $email_queue_replies_sent The number of replies sent to emails.
* @type int $email_queue_emails_created The number of new outgoing conversations created; Help Scout only.
* }
*/
public function get_stats( $args = array() ) {
Expand Down Expand Up @@ -153,9 +155,8 @@ public function get_stats( $args = array() ) {
"SELECT COUNT(*) FROM $wpdb->posts WHERE `post_type` = 'plugin' AND `post_status` = 'pending'"
);

// # Break down the plugins in the queue, based on if we're still waiting on a reply.
// Requires the helpscout tables to exist.
if ( defined( 'HELPSCOUT_PLUGINS_MAILBOXID' ) ) {
// Break down the plugins in the queue by whom we're waiting on; needs the helpscout tables, which either helpdesk fills.
if ( Helpdesk::is_freescout() || defined( 'HELPSCOUT_PLUGINS_MAILBOXID' ) ) {
$stats['in_queue_pending_why'] = $wpdb->get_row( $wpdb->prepare(
'SELECT
SUM( IF( active = 0 AND closed > 0, 1, 0 ) ) AS `author`,
Expand Down Expand Up @@ -199,10 +200,12 @@ public function get_stats( $args = array() ) {
) );

// --------------
// Help Scout Queue
// Email Queue
// --------------

if ( defined( 'HELPSCOUT_PLUGINS_MAILBOXID' ) ) {
if ( Helpdesk::is_freescout() ) {
$stats = array_merge( $stats, $this->get_freescout_queue_stats( $args ) );
} elseif ( defined( 'HELPSCOUT_PLUGINS_MAILBOXID' ) ) {
$start_datetime = gmdate( 'Y-m-d\T00:00:00\Z', strtotime( $args['date'] ) - ( $args['num_days'] * DAY_IN_SECONDS ) );
$end_datetime = gmdate( 'Y-m-d\T23:59:59\Z', strtotime( $args['date'] ) );

Expand All @@ -221,19 +224,60 @@ public function get_stats( $args = array() ) {
$company_report = $mailbox_overall = $email_report = false;
}

$stats['helpscout_queue_total_conversations'] = $mailbox_overall->current->totalConversations ?? 0;
$stats['helpscout_queue_new_conversations'] = $mailbox_overall->current->newConversations ?? 0;
$stats['helpscout_queue_customers'] = $mailbox_overall->current->customers ?? 0;
$stats['helpscout_queue_conversations_per_day'] = $mailbox_overall->current->conversationsPerDay ?? 0;
$stats['helpscout_queue_busiest_day'] = gmdate( 'l', strtotime( 'Sunday +' . ( $mailbox_overall->busiestDay->day ?? 0 ) . ' days' ) ); // Hacky? but works
$stats['helpscout_queue_messages_received'] = $mailbox_overall->current->messagesReceived ?? 0;
$stats['helpscout_queue_replies_sent'] = $company_report->current->totalReplies;
$stats['helpscout_queue_emails_created'] = $email_report->current->volume->emailsCreated ?? 0;
// phpcs:disable WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase -- Help Scout's API.
$stats['email_queue_total_conversations'] = $mailbox_overall->current->totalConversations ?? 0;
$stats['email_queue_new_conversations'] = $mailbox_overall->current->newConversations ?? 0;
$stats['email_queue_customers'] = $mailbox_overall->current->customers ?? 0;
$stats['email_queue_conversations_per_day'] = $mailbox_overall->current->conversationsPerDay ?? 0;
$stats['email_queue_busiest_day'] = gmdate( 'l', strtotime( 'Sunday +' . ( $mailbox_overall->busiestDay->day ?? 0 ) . ' days' ) ); // Hacky? but works.
$stats['email_queue_messages_received'] = $mailbox_overall->current->messagesReceived ?? 0;
$stats['email_queue_replies_sent'] = $company_report->current->totalReplies;
$stats['email_queue_emails_created'] = $email_report->current->volume->emailsCreated ?? 0;
// phpcs:enable
}

return $stats;
}

/**
* Gets the Plugins mailbox's queue stats from FreeScout's Reports module, through its API.
*
* FreeScout has no count of outgoing conversations, so there's no `email_queue_emails_created`.
*
* @param array $args The stats' date, and number of days before it.
* @return array The queue stats, see get_stats(); zeros if FreeScout's reports couldn't be fetched.
*/
protected function get_freescout_queue_stats( $args ) {
$query = array(
'filters' => array(
'from' => gmdate( 'Y-m-d', strtotime( $args['date'] ) - ( $args['num_days'] * DAY_IN_SECONDS ) ),
'to' => gmdate( 'Y-m-d', strtotime( $args['date'] ) ),
'mailbox' => Helpdesk::freescout_mailbox_id(),
),
);

$conversations = FreeScout::api( 'reports/conversations', $query );
$productivity = FreeScout::api( 'reports/productivity', $query );

// All zeros, so a failed request is obvious.
if ( ! isset( $conversations['metrics'], $productivity['metrics'] ) ) {
$conversations = array();
$productivity = array();
}

$metrics = $conversations['metrics'] ?? array();

return array(
'email_queue_total_conversations' => (int) ( $metrics['total']['value'] ?? 0 ),
'email_queue_new_conversations' => (int) ( $metrics['new']['value'] ?? 0 ),
'email_queue_customers' => (int) ( $metrics['customers']['value'] ?? 0 ),
'email_queue_conversations_per_day' => (int) ( $metrics['conv_day']['value'] ?? 0 ),
'email_queue_busiest_day' => (string) ( $metrics['busy_day']['value'] ?? '' ),
'email_queue_messages_received' => (int) ( $metrics['messages']['value'] ?? 0 ),
'email_queue_replies_sent' => (int) ( $productivity['metrics']['replies']['value'] ?? 0 ),
);
}

public function get_user_stats( $args = array() ) {
global $wpdb;

Expand Down Expand Up @@ -581,74 +625,84 @@ public function show_stats() {
<?php endif; ?>
</ul>

<?php if ( defined( 'HELPSCOUT_PLUGINS_MAILBOXID' ) ) : ?>
<h3><?php esc_html_e( 'Help Scout Queue Stats', 'wporg-plugins' ); ?></h3>
<?php if ( isset( $stats['email_queue_total_conversations'] ) ) : ?>
<h3>
<?php
printf(
/* translators: %s: Helpdesk name, like FreeScout. */
esc_html__( '%s Queue Stats', 'wporg-plugins' ),
esc_html( Helpdesk::name() )
);
?>
</h3>

<ul style="font-family:Courier New;">
<li>
<?php
printf(
'Total Conversations: %d',
esc_html( $stats['helpscout_queue_total_conversations'] )
esc_html( $stats['email_queue_total_conversations'] )
);
?>
</li>
<li>
<?php
printf(
'New Conversations: %d',
esc_html( $stats['helpscout_queue_new_conversations'] )
esc_html( $stats['email_queue_new_conversations'] )
);
?>
</li>
<li>
<?php
printf(
'Customers: %d',
esc_html( $stats['helpscout_queue_customers'] )
esc_html( $stats['email_queue_customers'] )
);
?>
</li>
<li>
<?php
printf(
'Conversations per Day: %d',
esc_html( $stats['helpscout_queue_conversations_per_day'] )
esc_html( $stats['email_queue_conversations_per_day'] )
);
?>
</li>
<li>
<?php
printf(
'Busiest Day: %s',
esc_html( $stats['helpscout_queue_busiest_day'] )
esc_html( $stats['email_queue_busiest_day'] )
);
?>
</li>
<li>
<?php
printf(
'Messages Received: %d',
esc_html( $stats['helpscout_queue_messages_received'] )
esc_html( $stats['email_queue_messages_received'] )
);
?>
</li>
<li>
<?php
printf(
'Replies Sent: %d',
esc_html( $stats['helpscout_queue_replies_sent'] )
);
?>
</li>
<li>
<?php
printf(
'Emails Created: %d',
esc_html( $stats['helpscout_queue_emails_created'] )
esc_html( $stats['email_queue_replies_sent'] )
);
?>
</li>
<?php if ( isset( $stats['email_queue_emails_created'] ) ) : ?>
<li>
<?php
printf(
'Emails Created: %d',
esc_html( $stats['email_queue_emails_created'] )
);
?>
</li>
<?php endif; ?>
</ul>
<?php endif; ?>

Expand Down Expand Up @@ -761,9 +815,9 @@ public function show_stats() {
?>
<ul style="font-style:italic;">
<?php if ( isset( $user_stats['all-hs-warning'] ) ) : ?>
<li><code>^</code> : This is of all Helpscout mailboxes, not Plugins specific, as plugins-only data is only available after <a href="https://meta.trac.wordpress.org/changeset/13010">2023-12-05</a>.</li>
<li><code>^</code> : This is of all Help Scout and FreeScout mailboxes, not Plugins specific, as plugins-only data is only available after <a href="https://meta.trac.wordpress.org/changeset/13010">2023-12-05</a>.</li>
<?php endif; ?>
<li><code>^</code> : Requires your Helpscout email to be the same as your WordPress.org email, or as one of your profiles alternate emails.</li>
<li><code>^</code> : Requires your Help Scout email to be the same as your WordPress.org email, or as one of your profiles alternate emails, and that you log in to FreeScout with your WordPress.org account.</li>
<li><code>^</code> : Email "Actions" include sending emails, replying to emails, marking as spam, moving to different inbox, etc.</li>
</ul>

Expand Down
Loading
Loading