Release v1.4.0 #146
3 changed files with 106 additions and 17 deletions
|
|
@ -37,46 +37,63 @@ public function publishToChannel(Article $article, array $extractedData, Platfor
|
|||
$authService = resolve(LemmyAuthService::class);
|
||||
$token = $authService->getToken($this->account);
|
||||
|
||||
$thumbnail = $this->hostedThumbnail($extractedData, $channel, $article, $token);
|
||||
|
||||
try {
|
||||
return $this->createPost($token, $extractedData, $channel, $article);
|
||||
return $this->createPost($token, $extractedData, $channel, $article, $thumbnail);
|
||||
} catch (Exception $e) {
|
||||
// If the cached token was stale, refresh and retry once
|
||||
if (str_contains($e->getMessage(), 'not_logged_in') || str_contains($e->getMessage(), 'Unauthorized')) {
|
||||
$token = $authService->refreshToken($this->account);
|
||||
|
||||
return $this->createPost($token, $extractedData, $channel, $article);
|
||||
return $this->createPost($token, $extractedData, $channel, $article, $thumbnail);
|
||||
}
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Uploaded once per publish, outside the stale-token retry: the upload is the expensive
|
||||
* part and a retry would otherwise re-download, re-encode and re-log.
|
||||
*
|
||||
* @param array<string, mixed> $extractedData
|
||||
*/
|
||||
private function hostedThumbnail(array $extractedData, PlatformChannel $channel, Article $article, string $token): ?string
|
||||
{
|
||||
$source = $extractedData['thumbnail'] ?? null;
|
||||
$source = is_string($source) && $source !== '' ? $source : null;
|
||||
|
||||
if ($source === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$hosted = $this->thumbnailUploader->upload($source, $token);
|
||||
|
||||
if ($hosted === null) {
|
||||
app(LogSaver::class)->warning('Thumbnail upload failed; publishing without one', $channel, [
|
||||
'article_id' => $article->id,
|
||||
'source' => $source,
|
||||
]);
|
||||
}
|
||||
|
||||
return $hosted;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $extractedData
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function createPost(string $token, array $extractedData, PlatformChannel $channel, Article $article): array
|
||||
private function createPost(string $token, array $extractedData, PlatformChannel $channel, Article $article, ?string $thumbnail = null): array
|
||||
{
|
||||
$languageId = $extractedData['language_id'] ?? null;
|
||||
|
||||
$thumbnail = $extractedData['thumbnail'] ?? null;
|
||||
$thumbnail = is_string($thumbnail) && $thumbnail !== '' ? $thumbnail : null;
|
||||
|
||||
$hosted = $this->thumbnailUploader->upload($thumbnail, $token);
|
||||
|
||||
if ($thumbnail !== null && $hosted === null) {
|
||||
resolve(LogSaver::class)->warning('Thumbnail upload failed; publishing without one', $channel, [
|
||||
'article_id' => $article->id,
|
||||
'source' => $thumbnail,
|
||||
]);
|
||||
}
|
||||
|
||||
return $this->api->createPost(
|
||||
$token,
|
||||
$extractedData['title'] ?? 'Untitled',
|
||||
$extractedData['description'] ?? '',
|
||||
$channel->channel_id,
|
||||
$article->url,
|
||||
$hosted,
|
||||
$thumbnail,
|
||||
$languageId
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -428,11 +428,75 @@ public function test_it_does_not_log_when_there_was_no_thumbnail_to_upload(): vo
|
|||
$apiMock->shouldReceive('createPost')->once()->andReturn(['ok' => true]);
|
||||
|
||||
$uploader = Mockery::mock(ThumbnailUploader::class);
|
||||
$uploader->shouldReceive('upload')->once()->with(null, 'tok')->andReturn(null);
|
||||
$uploader->shouldNotReceive('upload');
|
||||
|
||||
$publisher = new LemmyPublisher($account);
|
||||
$this->injectMocks($publisher, $apiMock, $uploader);
|
||||
|
||||
$this->assertSame(['ok' => true], $publisher->publishToChannel($article, ['title' => 'T'], $channel));
|
||||
}
|
||||
|
||||
public function test_a_stale_token_retry_does_not_re_upload_the_thumbnail(): void
|
||||
{
|
||||
$account = PlatformAccount::factory()->make(['instance_url' => 'https://lemmy.world']);
|
||||
$article = Article::factory()->create();
|
||||
$channel = PlatformChannel::factory()->create();
|
||||
|
||||
$authMock = Mockery::mock(LemmyAuthService::class);
|
||||
$authMock->shouldReceive('getToken')->once()->andReturn('stale');
|
||||
$authMock->shouldReceive('refreshToken')->once()->andReturn('fresh');
|
||||
$this->app->instance(LemmyAuthService::class, $authMock);
|
||||
|
||||
$apiMock = Mockery::mock(LemmyApiService::class);
|
||||
$apiMock->shouldReceive('createPost')
|
||||
->once()
|
||||
->with('stale', Mockery::any(), Mockery::any(), Mockery::any(), Mockery::any(), 'https://lemmy.world/pictrs/image/x.jpg', Mockery::any())
|
||||
->andThrow(new Exception('not_logged_in'));
|
||||
$apiMock->shouldReceive('createPost')
|
||||
->once()
|
||||
->with('fresh', Mockery::any(), Mockery::any(), Mockery::any(), Mockery::any(), 'https://lemmy.world/pictrs/image/x.jpg', Mockery::any())
|
||||
->andReturn(['ok' => true]);
|
||||
|
||||
$uploader = Mockery::mock(ThumbnailUploader::class);
|
||||
$uploader->shouldReceive('upload')->once()->andReturn('https://lemmy.world/pictrs/image/x.jpg');
|
||||
|
||||
$publisher = new LemmyPublisher($account);
|
||||
$this->injectMocks($publisher, $apiMock, $uploader);
|
||||
|
||||
$this->assertSame(
|
||||
['ok' => true],
|
||||
$publisher->publishToChannel($article, ['title' => 'T', 'thumbnail' => 'https://cdn.example.com/b.jpg'], $channel)
|
||||
);
|
||||
}
|
||||
|
||||
public function test_a_stale_token_retry_logs_the_upload_failure_only_once(): void
|
||||
{
|
||||
$account = PlatformAccount::factory()->make(['instance_url' => 'https://lemmy.world']);
|
||||
$article = Article::factory()->create();
|
||||
$channel = PlatformChannel::factory()->create();
|
||||
|
||||
$authMock = Mockery::mock(LemmyAuthService::class);
|
||||
$authMock->shouldReceive('getToken')->andReturn('stale');
|
||||
$authMock->shouldReceive('refreshToken')->andReturn('fresh');
|
||||
$this->app->instance(LemmyAuthService::class, $authMock);
|
||||
|
||||
$logMock = Mockery::mock(LogSaver::class);
|
||||
$logMock->shouldReceive('warning')->once();
|
||||
$this->app->instance(LogSaver::class, $logMock);
|
||||
|
||||
$apiMock = Mockery::mock(LemmyApiService::class);
|
||||
$apiMock->shouldReceive('createPost')->once()->andThrow(new Exception('not_logged_in'));
|
||||
$apiMock->shouldReceive('createPost')->once()->andReturn(['ok' => true]);
|
||||
|
||||
$uploader = Mockery::mock(ThumbnailUploader::class);
|
||||
$uploader->shouldReceive('upload')->once()->andReturn(null);
|
||||
|
||||
$publisher = new LemmyPublisher($account);
|
||||
$this->injectMocks($publisher, $apiMock, $uploader);
|
||||
|
||||
$this->assertSame(
|
||||
['ok' => true],
|
||||
$publisher->publishToChannel($article, ['title' => 'T', 'thumbnail' => 'https://cdn.example.com/b.jpg'], $channel)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -148,6 +148,14 @@ public function test_it_returns_null_when_the_source_exceeds_the_size_cap(): voi
|
|||
$this->assertNull($this->uploader()->upload('https://cdn.example.com/huge.jpg', 'token'));
|
||||
}
|
||||
|
||||
public function test_it_returns_null_when_the_source_exceeds_the_pixel_cap(): void
|
||||
{
|
||||
// A small file can still decode to a huge bitmap; this is the guard that protects the worker.
|
||||
$this->fake($this->jpeg(9000, 6000), ['files' => [['file' => 'abc123.jpg']]]);
|
||||
|
||||
$this->assertNull($this->uploader()->upload('https://cdn.example.com/huge-dimensions.jpg', 'token'));
|
||||
}
|
||||
|
||||
public function test_it_returns_null_when_the_download_throws(): void
|
||||
{
|
||||
Http::fake(fn () => throw new \RuntimeException('connection refused'));
|
||||
|
|
|
|||
Loading…
Reference in a new issue