150 - Delete unconvertible channels instead of resolving them via Lemmy
All checks were successful
CI / ci (push) Successful in 12m7s
All checks were successful
CI / ci (push) Successful in 12m7s
This commit is contained in:
parent
d17d98a5ec
commit
ddd560b204
2 changed files with 66 additions and 78 deletions
|
|
@ -1,26 +1,26 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
use App\Models\PlatformAccount;
|
|
||||||
use App\Modules\Lemmy\Services\LemmyApiService;
|
|
||||||
use Illuminate\Database\Migrations\Migration;
|
use Illuminate\Database\Migrations\Migration;
|
||||||
use Illuminate\Database\Schema\Blueprint;
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
use Illuminate\Support\Facades\DB;
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
use Illuminate\Support\Facades\Schema;
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* channel_id held a community slug, resolved to Lemmy's numeric id on every
|
* channel_id held a community slug, resolved to Lemmy's numeric id on every
|
||||||
* publish and every sync. It now holds that id directly; `name` remains the slug.
|
* publish and every sync. It now holds that id directly; `name` remains the slug.
|
||||||
|
*
|
||||||
|
* Slugs cannot be converted without asking the instance for the id, and a
|
||||||
|
* migration must not depend on a remote service: an earlier version of this file
|
||||||
|
* did, and a rate-limited login left this and eight later migrations unapplied
|
||||||
|
* across several releases. Channels still holding a slug are deleted instead, to
|
||||||
|
* be recreated through the UI, which validates the community on the instance.
|
||||||
*/
|
*/
|
||||||
return new class extends Migration
|
return new class extends Migration
|
||||||
{
|
{
|
||||||
public function up(): void
|
public function up(): void
|
||||||
{
|
{
|
||||||
// Every id is resolved before any DDL runs: these lookups hit the live
|
$this->deleteChannelsWithUnconvertibleIds();
|
||||||
// instance and MariaDB will not roll back a schema change if one fails.
|
|
||||||
$resolved = DB::table('platform_channels')
|
|
||||||
->orderBy('id')
|
|
||||||
->get()
|
|
||||||
->mapWithKeys(fn (object $channel) => [$channel->id => $this->resolve($channel)]);
|
|
||||||
|
|
||||||
Schema::table('platform_channels', function (Blueprint $table) {
|
Schema::table('platform_channels', function (Blueprint $table) {
|
||||||
$table->dropUnique('platform_channels_channel_id_unique');
|
$table->dropUnique('platform_channels_channel_id_unique');
|
||||||
|
|
@ -30,11 +30,9 @@ public function up(): void
|
||||||
$table->unsignedBigInteger('remote_community_id')->nullable()->after('channel_id');
|
$table->unsignedBigInteger('remote_community_id')->nullable()->after('channel_id');
|
||||||
});
|
});
|
||||||
|
|
||||||
foreach ($resolved as $id => $communityId) {
|
DB::table('platform_channels')->update([
|
||||||
DB::table('platform_channels')
|
'remote_community_id' => DB::raw('CAST(channel_id AS UNSIGNED)'),
|
||||||
->where('id', $id)
|
]);
|
||||||
->update(['remote_community_id' => $communityId]);
|
|
||||||
}
|
|
||||||
|
|
||||||
Schema::table('platform_channels', function (Blueprint $table) {
|
Schema::table('platform_channels', function (Blueprint $table) {
|
||||||
$table->dropColumn('channel_id');
|
$table->dropColumn('channel_id');
|
||||||
|
|
@ -67,33 +65,28 @@ public function down(): void
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private function resolve(object $channel): int
|
/**
|
||||||
|
* Routes, keywords, route articles and channel posts cascade from the
|
||||||
|
* database. article_publications does not have its foreign key until
|
||||||
|
* 000023, which deletes whatever this leaves orphaned.
|
||||||
|
*/
|
||||||
|
private function deleteChannelsWithUnconvertibleIds(): void
|
||||||
{
|
{
|
||||||
if (is_numeric($channel->channel_id)) {
|
$doomed = DB::table('platform_channels')
|
||||||
return (int) $channel->channel_id;
|
->get(['id', 'name', 'channel_id'])
|
||||||
|
->reject(fn (object $channel) => ctype_digit((string) $channel->channel_id))
|
||||||
|
->pluck('name', 'id');
|
||||||
|
|
||||||
|
if ($doomed->isEmpty()) {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$instance = DB::table('platform_instances')->find($channel->platform_instance_id);
|
Log::warning(sprintf(
|
||||||
|
'Deleting %d channel(s) whose community id is a slug rather than a numeric id: %s. Recreate them from the Channels page.',
|
||||||
|
$doomed->count(),
|
||||||
|
$doomed->implode(', '),
|
||||||
|
));
|
||||||
|
|
||||||
if (! $instance) {
|
DB::table('platform_channels')->whereIn('id', $doomed->keys())->delete();
|
||||||
throw new RuntimeException("Channel {$channel->id} has no platform instance; cannot resolve its community id.");
|
|
||||||
}
|
|
||||||
|
|
||||||
$account = PlatformAccount::where('instance_url', $instance->url)
|
|
||||||
->where('is_active', true)
|
|
||||||
->first();
|
|
||||||
|
|
||||||
if (! $account) {
|
|
||||||
throw new RuntimeException("No active account for {$instance->url}; cannot resolve community '{$channel->channel_id}'.");
|
|
||||||
}
|
|
||||||
|
|
||||||
$api = new LemmyApiService($instance->url);
|
|
||||||
$token = $api->login($account->username, $account->password);
|
|
||||||
|
|
||||||
if (! $token) {
|
|
||||||
throw new RuntimeException("Could not authenticate against {$instance->url} to resolve community '{$channel->channel_id}'.");
|
|
||||||
}
|
|
||||||
|
|
||||||
return $api->getCommunityId($channel->channel_id, $token);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@
|
||||||
|
|
||||||
namespace Tests\Feature;
|
namespace Tests\Feature;
|
||||||
|
|
||||||
use App\Models\PlatformAccount;
|
|
||||||
use App\Models\PlatformInstance;
|
use App\Models\PlatformInstance;
|
||||||
use Illuminate\Database\Schema\Blueprint;
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
|
@ -37,39 +36,31 @@ private function restoreSlugColumn(): void
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private function seedChannel(PlatformInstance $instance, string $slug): int
|
private function seedChannel(PlatformInstance $instance, string $channelId, string $name): int
|
||||||
{
|
{
|
||||||
return DB::table('platform_channels')->insertGetId([
|
return DB::table('platform_channels')->insertGetId([
|
||||||
'platform_instance_id' => $instance->id,
|
'platform_instance_id' => $instance->id,
|
||||||
'name' => $slug,
|
'name' => $name,
|
||||||
'display_name' => ucfirst($slug),
|
'display_name' => ucfirst($name),
|
||||||
'channel_id' => $slug,
|
'channel_id' => $channelId,
|
||||||
'is_active' => true,
|
'is_active' => true,
|
||||||
'created_at' => now(),
|
'created_at' => now(),
|
||||||
'updated_at' => now(),
|
'updated_at' => now(),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function instanceWithAccount(): PlatformInstance
|
private function prepare(): PlatformInstance
|
||||||
{
|
|
||||||
$instance = PlatformInstance::factory()->create(['url' => 'https://lemmy.test']);
|
|
||||||
PlatformAccount::factory()->create(['instance_url' => 'https://lemmy.test', 'is_active' => true]);
|
|
||||||
|
|
||||||
return $instance;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_it_replaces_the_slug_with_the_resolved_community_id(): void
|
|
||||||
{
|
{
|
||||||
$this->restoreSlugColumn();
|
$this->restoreSlugColumn();
|
||||||
DB::table('platform_channels')->delete();
|
DB::table('platform_channels')->delete();
|
||||||
|
|
||||||
$instance = $this->instanceWithAccount();
|
return PlatformInstance::factory()->create(['url' => 'https://lemmy.test']);
|
||||||
$id = $this->seedChannel($instance, 'news');
|
}
|
||||||
|
|
||||||
Http::fake([
|
public function test_it_keeps_a_channel_whose_id_is_already_numeric(): void
|
||||||
'*/api/v3/user/login*' => Http::response(['jwt' => 'token']),
|
{
|
||||||
'*/api/v3/community*' => Http::response(['community_view' => ['community' => ['id' => 8]]]),
|
$instance = $this->prepare();
|
||||||
]);
|
$id = $this->seedChannel($instance, '8', 'news');
|
||||||
|
|
||||||
$this->runMigration();
|
$this->runMigration();
|
||||||
|
|
||||||
|
|
@ -77,34 +68,38 @@ public function test_it_replaces_the_slug_with_the_resolved_community_id(): void
|
||||||
$this->assertSame('news', DB::table('platform_channels')->where('id', $id)->value('name'));
|
$this->assertSame('news', DB::table('platform_channels')->where('id', $id)->value('name'));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test_it_aborts_when_a_community_cannot_be_resolved(): void
|
public function test_it_deletes_a_channel_whose_id_is_still_a_slug(): void
|
||||||
{
|
{
|
||||||
$this->restoreSlugColumn();
|
$instance = $this->prepare();
|
||||||
DB::table('platform_channels')->delete();
|
$id = $this->seedChannel($instance, 'news', 'news');
|
||||||
|
|
||||||
$instance = $this->instanceWithAccount();
|
|
||||||
$this->seedChannel($instance, 'gone');
|
|
||||||
|
|
||||||
Http::fake([
|
|
||||||
'*/api/v3/user/login*' => Http::response(['jwt' => 'token']),
|
|
||||||
'*/api/v3/community*' => Http::response('not found', 404),
|
|
||||||
]);
|
|
||||||
|
|
||||||
$this->expectException(\Exception::class);
|
|
||||||
|
|
||||||
$this->runMigration();
|
$this->runMigration();
|
||||||
|
|
||||||
|
$this->assertDatabaseMissing('platform_channels', ['id' => $id]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test_it_aborts_when_the_instance_has_no_active_account(): void
|
public function test_it_keeps_numeric_channels_while_deleting_slug_ones(): void
|
||||||
{
|
{
|
||||||
$this->restoreSlugColumn();
|
$instance = $this->prepare();
|
||||||
DB::table('platform_channels')->delete();
|
$kept = $this->seedChannel($instance, '8', 'news');
|
||||||
|
$deleted = $this->seedChannel($instance, 'nieuws', 'nieuws');
|
||||||
$instance = PlatformInstance::factory()->create(['url' => 'https://no-account.test']);
|
|
||||||
$this->seedChannel($instance, 'news');
|
|
||||||
|
|
||||||
$this->expectException(\RuntimeException::class);
|
|
||||||
|
|
||||||
$this->runMigration();
|
$this->runMigration();
|
||||||
|
|
||||||
|
$this->assertDatabaseHas('platform_channels', ['id' => $kept]);
|
||||||
|
$this->assertDatabaseMissing('platform_channels', ['id' => $deleted]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_it_makes_no_http_requests(): void
|
||||||
|
{
|
||||||
|
Http::preventStrayRequests();
|
||||||
|
|
||||||
|
$instance = $this->prepare();
|
||||||
|
$this->seedChannel($instance, 'news', 'news');
|
||||||
|
$this->seedChannel($instance, '8', 'other');
|
||||||
|
|
||||||
|
$this->runMigration();
|
||||||
|
|
||||||
|
Http::assertNothingSent();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue