Repository navigation
FreeScout: Open HelpScout's links to conversations in FreeScout #1082
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Closed
Closed
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
50 changes: 50 additions & 0 deletions
50
.../Database/Migrations/2026_10_20_000000_create_wporgsite_helpscout_conversations_table.php
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,50 @@ | ||
| <?php | ||
| /** | ||
| * Creates the table of HelpScout's conversation IDs. | ||
| * | ||
| * @package WordPressdotorg\FreeScout\WPOrgSite | ||
| */ | ||
|
|
||
| declare( strict_types = 1 ); | ||
|
|
||
| use Illuminate\Database\Migrations\Migration; | ||
| use Illuminate\Database\Schema\Blueprint; | ||
| use Illuminate\Support\Facades\Schema; | ||
| use Modules\WPOrgSite\Services\HelpScoutLinks; | ||
|
|
||
| /** | ||
| * The FreeScout conversation each HelpScout conversation was imported as, for HelpScout's links. | ||
| */ | ||
| class CreateWporgsiteHelpscoutConversationsTable extends Migration { | ||
|
|
||
| /** | ||
| * Runs the migration. | ||
| * | ||
| * @return void | ||
| */ | ||
| public function up(): void { | ||
| if ( Schema::hasTable( HelpScoutLinks::TABLE ) ) { | ||
| return; | ||
| } | ||
|
|
||
| Schema::create( | ||
| HelpScoutLinks::TABLE, | ||
| static function ( Blueprint $table ): void { | ||
| $table->unsignedBigInteger( 'helpscout_id' )->primary(); | ||
| $table->unsignedInteger( 'conversation_id' )->index(); | ||
| } | ||
| ); | ||
|
|
||
| // Later imports are recorded as the importer saves them. | ||
| HelpScoutLinks::copy_imported(); | ||
| } | ||
|
|
||
| /** | ||
| * Reverts the migration. | ||
| * | ||
| * @return void | ||
| */ | ||
| public function down(): void { | ||
| Schema::dropIfExists( HelpScoutLinks::TABLE ); | ||
| } | ||
| } |
39 changes: 39 additions & 0 deletions
39
freescout.wordpress.net/Modules/WPOrgSite/Http/Controllers/HelpScoutLinksController.php
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,39 @@ | ||
| <?php | ||
| /** | ||
| * Opens HelpScout's links to conversations. | ||
| * | ||
| * @package WordPressdotorg\FreeScout\WPOrgSite | ||
| */ | ||
|
|
||
| declare( strict_types = 1 ); | ||
|
|
||
| namespace Modules\WPOrgSite\Http\Controllers; | ||
|
|
||
| use App\User; | ||
| use Illuminate\Http\RedirectResponse; | ||
| use Illuminate\Http\Request; | ||
| use Illuminate\Routing\Controller; | ||
| use Modules\WPOrgSite\Services\HelpScoutLinks; | ||
|
|
||
| /** | ||
| * Redirects `/conversation/{HelpScout ID}/{number}`, a HelpScout link with FreeScout's address, to the conversation. | ||
| */ | ||
| final class HelpScoutLinksController extends Controller { | ||
|
|
||
| /** | ||
| * Redirects to the conversation the HelpScout conversation was imported as. | ||
| * | ||
| * @param Request $request Request. | ||
| * @param string $helpscout_id HelpScout's conversation ID. | ||
| * @return RedirectResponse | ||
| */ | ||
| public function show( Request $request, string $helpscout_id ): RedirectResponse { | ||
| $user = $request->user(); | ||
| $conversation = $user instanceof User ? HelpScoutLinks::conversation( (int) $helpscout_id, $user ) : null; | ||
| if ( ! $conversation ) { | ||
| abort( 404 ); | ||
| } | ||
|
|
||
| return redirect()->away( $conversation->url() ); | ||
| } | ||
| } |
51 changes: 51 additions & 0 deletions
51
freescout.wordpress.net/Modules/WPOrgSite/Http/Middleware/RedirectHelpScoutLinks.php
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,51 @@ | ||
| <?php | ||
| /** | ||
| * Redirects HelpScout's conversation IDs in core's conversation links. | ||
| * | ||
| * @package WordPressdotorg\FreeScout\WPOrgSite | ||
| */ | ||
|
|
||
| declare( strict_types = 1 ); | ||
|
|
||
| namespace Modules\WPOrgSite\Http\Middleware; | ||
|
|
||
| use App\User; | ||
| use Closure; | ||
| use Illuminate\Http\Request; | ||
| use Modules\WPOrgSite\Services\HelpScoutLinks; | ||
| use Symfony\Component\HttpFoundation\Response; | ||
|
|
||
| /** | ||
| * Redirects `/conversation/{HelpScout ID}`, which core answers with a 404, to the conversation it was imported as. | ||
| * | ||
| * It acts only on core's 404, so viewing a FreeScout conversation costs nothing extra. | ||
| */ | ||
| final class RedirectHelpScoutLinks { | ||
|
|
||
| /** | ||
| * Handles a request. | ||
| * | ||
| * @param Request $request Request. | ||
| * @param Closure $next Next handler. | ||
| * @return Response | ||
| */ | ||
| public function handle( Request $request, Closure $next ): Response { | ||
| $response = $next( $request ); | ||
| $route = $request->route(); | ||
| $user = $request->user(); | ||
|
|
||
| if ( 404 !== $response->getStatusCode() || ! $route || 'conversations.view' !== $route->getName() || ! $user instanceof User ) { | ||
| return $response; | ||
| } | ||
|
|
||
| try { | ||
| $id = (int) $route->parameter( 'id' ); | ||
| $conversation = $id ? HelpScoutLinks::conversation( $id, $user ) : null; | ||
| } catch ( \Throwable $e ) { | ||
| \Log::error( '[WPOrgSite] Could not look up a HelpScout conversation: ' . $e->getMessage() ); | ||
| $conversation = null; | ||
| } | ||
|
|
||
| return $conversation ? redirect()->away( $conversation->url() ) : $response; | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,27 @@ | ||
| <?php | ||
| /** | ||
| * WPOrgSite routes. | ||
| * | ||
| * @package WordPressdotorg\FreeScout\WPOrgSite | ||
| */ | ||
|
|
||
| declare( strict_types = 1 ); | ||
|
|
||
| Route::group( | ||
| array( | ||
| 'middleware' => array( 'web', 'auth' ), | ||
| 'prefix' => \Helper::getSubdirectory(), | ||
| 'namespace' => 'Modules\WPOrgSite\Http\Controllers', | ||
| ), | ||
| static function (): void { | ||
| // `/conversation/{ID}` alone is core's route, handled by the RedirectHelpScoutLinks middleware. | ||
| Route::get( '/conversation/{helpscout_id}/{number}', 'HelpScoutLinksController@show' ) | ||
| ->where( | ||
| array( | ||
| 'helpscout_id' => '[0-9]+', | ||
| 'number' => '[0-9]+', | ||
| ) | ||
| ) | ||
| ->name( 'wporgsite.helpscout_link' ); | ||
| } | ||
| ); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
82 changes: 82 additions & 0 deletions
82
freescout.wordpress.net/Modules/WPOrgSite/Services/HelpScoutLinks.php
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,82 @@ | ||
| <?php | ||
| /** | ||
| * HelpScout's links to conversations. | ||
| * | ||
| * @package WordPressdotorg\FreeScout\WPOrgSite | ||
| */ | ||
|
|
||
| declare( strict_types = 1 ); | ||
|
|
||
| namespace Modules\WPOrgSite\Services; | ||
|
|
||
| use App\Conversation; | ||
| use App\User; | ||
| use Illuminate\Support\Facades\Schema; | ||
|
|
||
| /** | ||
| * Finds the FreeScout conversation a HelpScout conversation was imported as, from the HelpScout ID in its links. | ||
| * | ||
| * The importer's table of IDs goes with the importer, so this keeps a copy. | ||
| */ | ||
| final class HelpScoutLinks { | ||
|
|
||
| /** | ||
| * The table of HelpScout IDs and the conversations they were imported as. | ||
| * | ||
| * @var string | ||
| */ | ||
| public const TABLE = 'wporgsite_helpscout_conversations'; | ||
|
|
||
| /** | ||
| * The importer's table, copied when this one is created. | ||
| * | ||
| * @var string | ||
| */ | ||
| public const IMPORTER_TABLE = 'wporghelpscoutimport_conversations'; | ||
|
|
||
| /** | ||
| * The importer's model of that table; saving one adds it here. | ||
| * | ||
| * @var string | ||
| */ | ||
| public const IMPORTER_MODEL = 'Modules\WPOrgHelpScoutImport\Entities\ImportedConversation'; | ||
|
|
||
| /** | ||
| * Records the conversation a HelpScout conversation was imported as. | ||
| * | ||
| * @param int $helpscout_id HelpScout's conversation ID. | ||
| * @param int $conversation_id FreeScout's conversation ID. | ||
| * @return void | ||
| */ | ||
| public static function record( int $helpscout_id, int $conversation_id ): void { | ||
| \DB::table( self::TABLE )->updateOrInsert( array( 'helpscout_id' => $helpscout_id ), array( 'conversation_id' => $conversation_id ) ); | ||
| } | ||
|
|
||
| /** | ||
| * Copies the IDs the importer has recorded so far into the new table. | ||
| * | ||
| * @return void | ||
| */ | ||
| public static function copy_imported(): void { | ||
| if ( ! Schema::hasTable( self::IMPORTER_TABLE ) ) { | ||
| return; | ||
| } | ||
|
|
||
| $grammar = \DB::getQueryGrammar(); | ||
| \DB::statement( 'INSERT INTO ' . $grammar->wrapTable( self::TABLE ) . ' (helpscout_id, conversation_id) SELECT helpscout_id, conversation_id FROM ' . $grammar->wrapTable( self::IMPORTER_TABLE ) ); | ||
| } | ||
|
|
||
| /** | ||
| * The conversation a HelpScout conversation was imported as, if the agent may see it. | ||
| * | ||
| * @param int $helpscout_id HelpScout's conversation ID. | ||
| * @param User $user Agent. | ||
| * @return Conversation|null The conversation, or null if there's none they may see. | ||
| */ | ||
| public static function conversation( int $helpscout_id, User $user ): ?Conversation { | ||
| $conversation_id = \DB::table( self::TABLE )->where( 'helpscout_id', $helpscout_id )->value( 'conversation_id' ); | ||
| $conversation = $conversation_id ? Conversation::find( (int) $conversation_id ) : null; | ||
|
|
||
| return $conversation instanceof Conversation && $user->can( 'view', $conversation ) ? $conversation : null; | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win
🔎 Supported by static analysis
🏁 Script executed:
Repository: WordPress/wordpress.org
Length of output: 3503
🏁 Script executed:
Repository: WordPress/wordpress.org
Length of output: 20612
🏁 Script executed:
Repository: WordPress/wordpress.org
Length of output: 4537
Make the importer copy idempotent.
If an importer save occurs after the destination table is created and before
copy_imported()runs, the saved listener can insert the samehelpscout_id. The plainINSERTthen fails on the destination table's primary key. A retry skips the copy because the table already exists.Create the table only when it is missing, then always call
copy_imported(). Skip IDs already present in the destination table. The importer schema makesconversation_idnon-null, so no null-row filter is needed.🛡️ Suggested fix
public function up(): void { - if ( Schema::hasTable( HelpScoutLinks::TABLE ) ) { - return; + if ( ! Schema::hasTable( HelpScoutLinks::TABLE ) ) { + Schema::create( + HelpScoutLinks::TABLE, + static function ( Blueprint $table ): void { + $table->unsignedBigInteger( 'helpscout_id' )->primary(); + $table->unsignedInteger( 'conversation_id' )->index(); + } + ); } - Schema::create( - HelpScoutLinks::TABLE, - static function ( Blueprint $table ): void { - $table->unsignedBigInteger( 'helpscout_id' )->primary(); - $table->unsignedInteger( 'conversation_id' )->index(); - } - ); - - // The importer adds the ones it imports from now on. HelpScoutLinks::copy_imported(); }🤖 Prompt for AI Agents
Source: Coding guidelines