Files
landing-api/app/Services/TaskStream/TaskStreamClient.php
Josep Piguillem 7e7d2847fe
Some checks failed
Tests / lint (push) Has been cancelled
Tests / test (push) Has been cancelled
Tests / test-postgres (push) Has been cancelled
Tests / mcp (push) Has been cancelled
Point the public site to each project's TaskStream board
A project with a board now carries its key, its address and the address
of the board's public showcase in the public API, so the website can show
its roadmap, releases and tickets for newcomers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 19:13:56 +02:00

106 lines
3.1 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Services\TaskStream;
use Illuminate\Http\Client\ConnectionException;
use Illuminate\Support\Facades\Http;
/**
* The few calls this application makes to TaskStream's REST API.
*
* Every call is made as somebody: TaskStream accepts the same Pireon tokens
* this application does, and whoever's token it is becomes the owner of
* what is created.
*/
class TaskStreamClient
{
public function __construct(
protected string $apiUrl,
protected string $webUrl,
protected ?string $serviceToken = null,
protected int $timeout = 10,
) {}
/**
* @param array<string, mixed> $config
*/
public static function fromConfig(array $config): self
{
return new self(
apiUrl: rtrim((string) ($config['api_url'] ?? ''), '/'),
webUrl: rtrim((string) ($config['web_url'] ?? ''), '/'),
serviceToken: filled($config['token'] ?? null) ? $config['token'] : null,
timeout: (int) ($config['timeout'] ?? 10),
);
}
/**
* The token for requests that do not carry a Pireon token of their own.
*/
public function serviceToken(): ?string
{
return $this->serviceToken;
}
/**
* @return array{id: string, key: string}
*
* @throws TaskStreamException
*/
public function createProject(string $token, string $key, string $name, ?string $description, bool $public): array
{
try {
$response = Http::withToken($token)
->acceptJson()
->timeout($this->timeout)
->post("{$this->apiUrl}/api/projects", array_filter([
'key' => $key,
'name' => $name,
'description' => $description,
'isPublic' => $public,
], fn ($value) => $value !== null));
} catch (ConnectionException $e) {
throw TaskStreamException::unreachable($this->apiUrl, $e);
}
if ($response->failed()) {
// Nest answers `message` as a string, or as a list from validation.
$message = $response->json('message');
throw new TaskStreamException(
is_array($message) ? implode(' ', $message) : (string) ($message ?: $response->reason()),
status: $response->status(),
);
}
return ['id' => (string) $response->json('id'), 'key' => (string) $response->json('key')];
}
/**
* Where people open a project's board.
*/
public function projectUrl(string $key): string
{
return "{$this->webUrl}/p/{$key}";
}
/**
* The public summary of a board (roadmap, releases, sprint, tickets for
* newcomers). Anyone may read it, so the public site can ask directly.
*/
public function showcaseUrl(string $key): string
{
return "{$this->apiUrl}/api/explore/projects/{$key}/showcase";
}
/**
* Where the paths of a showcase (`/p/KEY/issues/KEY-3`) are opened.
*/
public function webUrl(): string
{
return $this->webUrl;
}
}