Files
hyprcrd/public/plugins/gitea/backend.php
T
thandClaude Opus 4.6 04b52ecc19 gitea: default repo setting; editor: asset picker with thumbnail grid
Gitea plugin: default_repo field per account (stored in DB, pre-fills
create issue form but stays editable). Editor toolbar: asset picker
flyout with 4-column thumbnail grid and live search filter. Clicking
a tile inserts the matching markdown.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 19:26:04 +02:00

628 lines
28 KiB
PHP

<?php
/**
* Gitea Plugin: backend.
*
* Replaces the standalone Python `gitea-issue-client` tool. MVP scope:
* accounts CRUD + connection test. Issue browsing/extraction comes once
* the connection layer is verified. See TODO.md for the deferred items.
*
* Routes:
* GET /api/gitea/accounts → list accounts (no tokens)
* POST /api/gitea/accounts → create account
* PATCH /api/gitea/accounts/{id} → update (omit token to keep)
* DELETE /api/gitea/accounts/{id} → remove
* POST /api/gitea/accounts/{id}/test → verify token via /api/v1/user
*/
require_once __DIR__ . '/GiteaClient.php';
// ── Schema migration ──────────────────────────────────────────────
(function () {
$db = Database::connect();
$db->exec("
CREATE TABLE IF NOT EXISTS gitea_accounts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
base_url TEXT NOT NULL,
token TEXT NOT NULL,
default_repo TEXT DEFAULT NULL,
position INTEGER NOT NULL DEFAULT 0,
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
)
");
// Migration: add default_repo column if missing
$cols = $db->query("PRAGMA table_info(gitea_accounts)")->fetchAll(PDO::FETCH_COLUMN, 1);
if (!in_array('default_repo', $cols)) {
$db->exec("ALTER TABLE gitea_accounts ADD COLUMN default_repo TEXT DEFAULT NULL");
}
})();
// ── Encryption helpers (mirrors email plugin / CalDavSync) ────────
function giteaEncKey(): string {
$db = Database::connect();
$vk = $db->query("SELECT value FROM settings WHERE key = 'vault_key'")->fetchColumn();
if ($vk) return substr(hash('sha256', $vk), 0, 32);
$hash = $db->query("SELECT value FROM settings WHERE key = 'password_hash'")->fetchColumn();
return substr(hash('sha256', $hash ?: 'hyprcrd-default-key'), 0, 32);
}
function giteaEncrypt(string $plain): string {
$key = giteaEncKey();
$iv = random_bytes(16);
$cipher = openssl_encrypt($plain, 'aes-256-cbc', $key, OPENSSL_RAW_DATA, $iv);
return base64_encode($iv . $cipher);
}
function giteaDecrypt(string $encoded): string {
$key = giteaEncKey();
$raw = base64_decode($encoded);
if (!$raw || strlen($raw) < 17) return '';
$iv = substr($raw, 0, 16);
$cipher = substr($raw, 16);
return openssl_decrypt($cipher, 'aes-256-cbc', $key, OPENSSL_RAW_DATA, $iv) ?: '';
}
function giteaJson($data, int $code = 200): void {
http_response_code($code);
header('Content-Type: application/json');
echo json_encode($data);
exit;
}
function giteaGetAccount(int $id): ?array {
$db = Database::connect();
$stmt = $db->prepare('SELECT * FROM gitea_accounts WHERE id = :id');
$stmt->execute(['id' => $id]);
return $stmt->fetch() ?: null;
}
function giteaOpenClient(array $row): GiteaClient {
return new GiteaClient($row['base_url'], giteaDecrypt($row['token']));
}
// ── Route registration ────────────────────────────────────────────
PluginManager::instance()->onApiRoute(function (string $route, string $method) {
// ── Accounts CRUD ──
if ($route === '/api/gitea/accounts' && $method === 'GET') {
$db = Database::connect();
$rows = $db->query('SELECT id, name, base_url, default_repo, position, created_at FROM gitea_accounts ORDER BY position ASC, id ASC')->fetchAll();
giteaJson($rows);
}
if ($route === '/api/gitea/accounts' && $method === 'POST') {
$input = json_decode(file_get_contents('php://input'), true) ?: [];
$name = trim((string)($input['name'] ?? ''));
$baseUrl = trim((string)($input['base_url'] ?? ''));
$token = (string)($input['token'] ?? '');
if ($name === '' || $baseUrl === '' || $token === '') {
giteaJson(['error' => 'name, base_url, and token are required'], 400);
}
// Normalize base URL: strip trailing slash, accept with/without scheme
if (!preg_match('#^https?://#i', $baseUrl)) {
$baseUrl = 'https://' . $baseUrl;
}
$baseUrl = rtrim($baseUrl, '/');
$db = Database::connect();
$stmt = $db->prepare('
INSERT INTO gitea_accounts (name, base_url, token)
VALUES (:n, :u, :t)
');
$stmt->execute([
'n' => $name,
'u' => $baseUrl,
't' => giteaEncrypt($token),
]);
giteaJson(['ok' => true, 'id' => (int)$db->lastInsertId()]);
}
if (preg_match('#^/api/gitea/accounts/(\d+)$#', $route, $m) && $method === 'PATCH') {
$id = (int)$m[1];
$row = giteaGetAccount($id);
if (!$row) giteaJson(['error' => 'account not found'], 404);
$input = json_decode(file_get_contents('php://input'), true) ?: [];
$fields = []; $params = ['id' => $id];
if (array_key_exists('name', $input)) {
$fields[] = 'name = :name'; $params['name'] = trim((string)$input['name']);
}
if (array_key_exists('base_url', $input)) {
$u = trim((string)$input['base_url']);
if (!preg_match('#^https?://#i', $u)) $u = 'https://' . $u;
$fields[] = 'base_url = :base_url'; $params['base_url'] = rtrim($u, '/');
}
if (!empty($input['token'])) {
$fields[] = 'token = :token'; $params['token'] = giteaEncrypt($input['token']);
}
if (array_key_exists('default_repo', $input)) {
$val = trim((string)($input['default_repo'] ?? ''));
$fields[] = 'default_repo = :default_repo'; $params['default_repo'] = $val ?: null;
}
if (!$fields) giteaJson(['ok' => true]);
$db = Database::connect();
$db->prepare('UPDATE gitea_accounts SET ' . implode(', ', $fields) . ' WHERE id = :id')->execute($params);
giteaJson(['ok' => true]);
}
if (preg_match('#^/api/gitea/accounts/(\d+)$#', $route, $m) && $method === 'DELETE') {
$id = (int)$m[1];
$db = Database::connect();
$db->prepare('DELETE FROM gitea_accounts WHERE id = :id')->execute(['id' => $id]);
giteaJson(['ok' => true]);
}
// ── Issues: list ──
if ($route === '/api/gitea/issues' && $method === 'GET') {
$accId = (int)($_GET['account'] ?? 0);
$row = giteaGetAccount($accId);
if (!$row) giteaJson(['error' => 'account not found'], 404);
$state = in_array($_GET['state'] ?? 'open', ['open', 'closed', 'all'], true) ? $_GET['state'] : 'open';
$q = trim((string)($_GET['q'] ?? ''));
$owner = trim((string)($_GET['owner'] ?? ''));
$scope = $_GET['scope'] ?? 'all'; // all | assigned | created | mentioned
$page = max(1, (int)($_GET['page'] ?? 1));
$limit = max(5, min(100, (int)($_GET['limit'] ?? 30)));
try {
$client = giteaOpenClient($row);
$issues = $client->searchIssues([
'state' => $state,
'q' => $q !== '' ? $q : null,
'owner' => $owner !== '' ? $owner : null,
'type' => 'issues', // exclude PRs
'assigned' => $scope === 'assigned' ? true : null,
'created' => $scope === 'created' ? true : null,
'mentioned' => $scope === 'mentioned' ? true : null,
'page' => $page,
'limit' => $limit,
]);
// Map to a compact, UI-friendly shape
$mapped = array_map(function ($i) {
$repoFull = $i['repository']['full_name'] ?? null;
if (!$repoFull && isset($i['repository']['owner'], $i['repository']['name'])) {
$repoFull = $i['repository']['owner'] . '/' . $i['repository']['name'];
}
return [
'id' => $i['id'] ?? null,
'number' => $i['number'] ?? null,
'title' => $i['title'] ?? '',
'state' => $i['state'] ?? 'open',
'created_at' => $i['created_at'] ?? null,
'updated_at' => $i['updated_at'] ?? null,
'comments_count' => $i['comments'] ?? 0,
'html_url' => $i['html_url'] ?? null,
'repo' => $repoFull,
'author' => $i['user']['login'] ?? null,
'labels' => array_map(fn($l) => [
'name' => $l['name'] ?? '',
'color' => $l['color'] ?? '888888',
], $i['labels'] ?? []),
'assignees' => array_map(fn($a) => $a['login'] ?? '', $i['assignees'] ?? []),
];
}, $issues);
giteaJson([
'ok' => true,
'issues' => $mapped,
'page' => $page,
'limit' => $limit,
'hasMore' => count($mapped) >= $limit, // heuristic — Gitea doesn't always return total
]);
} catch (GiteaApiException $e) {
giteaJson(['ok' => false, 'error' => "HTTP {$e->statusCode}: " . substr($e->body, 0, 300)], 200);
} catch (\Throwable $e) {
giteaJson(['ok' => false, 'error' => $e->getMessage()], 200);
}
}
// ── Issue detail (with comments) ──
if (preg_match('#^/api/gitea/issue/(\d+)/([^/]+)/([^/]+)/(\d+)$#', $route, $m) && $method === 'GET') {
$accId = (int)$m[1];
$owner = urldecode($m[2]);
$repo = urldecode($m[3]);
$index = (int)$m[4];
$row = giteaGetAccount($accId);
if (!$row) giteaJson(['error' => 'account not found'], 404);
try {
$client = giteaOpenClient($row);
$issue = $client->getIssue($owner, $repo, $index);
$comments = $client->listIssueComments($owner, $repo, $index);
giteaJson([
'ok' => true,
'issue' => [
'id' => $issue['id'] ?? null,
'number' => $issue['number'] ?? $index,
'title' => $issue['title'] ?? '',
'body' => $issue['body'] ?? '',
'state' => $issue['state'] ?? 'open',
'created_at' => $issue['created_at'] ?? null,
'updated_at' => $issue['updated_at'] ?? null,
'closed_at' => $issue['closed_at'] ?? null,
'due_date' => $issue['due_date'] ?? null,
'html_url' => $issue['html_url'] ?? null,
'author' => $issue['user']['login'] ?? null,
'author_avatar' => $issue['user']['avatar_url'] ?? null,
'repo' => "$owner/$repo",
'owner' => $owner,
'repo_name' => $repo,
'labels' => array_map(fn($l) => [
'name' => $l['name'] ?? '',
'color' => $l['color'] ?? '888888',
], $issue['labels'] ?? []),
'assignees' => array_map(fn($a) => $a['login'] ?? '', $issue['assignees'] ?? []),
'milestone' => isset($issue['milestone']) && $issue['milestone'] ? [
'id' => $issue['milestone']['id'] ?? null,
'title' => $issue['milestone']['title'] ?? '',
] : null,
],
'comments' => array_map(fn($c) => [
'id' => $c['id'] ?? null,
'author' => $c['user']['login'] ?? null,
'body' => $c['body'] ?? '',
'created_at' => $c['created_at'] ?? null,
'updated_at' => $c['updated_at'] ?? null,
], $comments),
]);
} catch (GiteaApiException $e) {
giteaJson(['ok' => false, 'error' => "HTTP {$e->statusCode}: " . substr($e->body, 0, 300)], 200);
} catch (\Throwable $e) {
giteaJson(['ok' => false, 'error' => $e->getMessage()], 200);
}
}
// ── Issue write operations ──
if (preg_match('#^/api/gitea/issue/(\d+)/([^/]+)/([^/]+)/(\d+)/comment$#', $route, $m) && $method === 'POST') {
$accId = (int)$m[1]; $owner = urldecode($m[2]); $repo = urldecode($m[3]); $index = (int)$m[4];
$row = giteaGetAccount($accId);
if (!$row) giteaJson(['error' => 'account not found'], 404);
$input = json_decode(file_get_contents('php://input'), true) ?: [];
$body = trim((string)($input['body'] ?? ''));
if ($body === '') giteaJson(['error' => 'comment body required'], 400);
try {
$client = giteaOpenClient($row);
$created = $client->createIssueComment($owner, $repo, $index, $body);
giteaJson([
'ok' => true,
'comment' => [
'id' => $created['id'] ?? null,
'author' => $created['user']['login'] ?? null,
'body' => $created['body'] ?? '',
'created_at' => $created['created_at'] ?? null,
],
]);
} catch (GiteaApiException $e) {
giteaJson(['ok' => false, 'error' => "HTTP {$e->statusCode}: " . substr($e->body, 0, 300)], 200);
} catch (\Throwable $e) {
giteaJson(['ok' => false, 'error' => $e->getMessage()], 200);
}
}
if (preg_match('#^/api/gitea/issue/(\d+)/([^/]+)/([^/]+)/(\d+)$#', $route, $m) && $method === 'PATCH') {
$accId = (int)$m[1]; $owner = urldecode($m[2]); $repo = urldecode($m[3]); $index = (int)$m[4];
$row = giteaGetAccount($accId);
if (!$row) giteaJson(['error' => 'account not found'], 404);
$input = json_decode(file_get_contents('php://input'), true) ?: [];
$changes = [];
foreach (['title', 'body', 'state', 'assignees', 'milestone', 'due_date', 'unset_due_date'] as $k) {
if (array_key_exists($k, $input)) $changes[$k] = $input[$k];
}
if (!$changes) giteaJson(['error' => 'no changes'], 400);
if (isset($changes['state']) && !in_array($changes['state'], ['open', 'closed'], true)) {
giteaJson(['error' => 'invalid state'], 400);
}
try {
$client = giteaOpenClient($row);
$updated = $client->editIssue($owner, $repo, $index, $changes);
giteaJson([
'ok' => true,
'issue' => [
'title' => $updated['title'] ?? '',
'body' => $updated['body'] ?? '',
'state' => $updated['state'] ?? 'open',
'updated_at' => $updated['updated_at'] ?? null,
'closed_at' => $updated['closed_at'] ?? null,
],
]);
} catch (GiteaApiException $e) {
giteaJson(['ok' => false, 'error' => "HTTP {$e->statusCode}: " . substr($e->body, 0, 300)], 200);
} catch (\Throwable $e) {
giteaJson(['ok' => false, 'error' => $e->getMessage()], 200);
}
}
if (preg_match('#^/api/gitea/issue/(\d+)/([^/]+)/([^/]+)/(\d+)/labels$#', $route, $m) && $method === 'PUT') {
$accId = (int)$m[1]; $owner = urldecode($m[2]); $repo = urldecode($m[3]); $index = (int)$m[4];
$row = giteaGetAccount($accId);
if (!$row) giteaJson(['error' => 'account not found'], 404);
$input = json_decode(file_get_contents('php://input'), true) ?: [];
$labels = $input['labels'] ?? [];
if (!is_array($labels)) giteaJson(['error' => 'labels array required'], 400);
try {
$client = giteaOpenClient($row);
$resulting = $client->replaceIssueLabels($owner, $repo, $index, $labels);
giteaJson([
'ok' => true,
'labels' => array_map(fn($l) => [
'id' => $l['id'] ?? null,
'name' => $l['name'] ?? '',
'color' => $l['color'] ?? '888888',
], $resulting),
]);
} catch (GiteaApiException $e) {
giteaJson(['ok' => false, 'error' => "HTTP {$e->statusCode}: " . substr($e->body, 0, 300)], 200);
} catch (\Throwable $e) {
giteaJson(['ok' => false, 'error' => $e->getMessage()], 200);
}
}
// ── Attachment proxy ──
// Gitea attachments live under /attachments/<uuid> on the gitea instance
// and require the auth token (or session cookie). The browser can't fetch
// them directly (CORS + no token), so hyprcrd proxies them.
if (preg_match('#^/api/gitea/attachment/(\d+)/(.+)$#', $route, $m) && $method === 'GET') {
$accId = (int)$m[1];
$path = $m[2]; // e.g. "attachments/abc-123-uuid" or "user/repo/raw/branch/file.png"
$row = giteaGetAccount($accId);
if (!$row) { http_response_code(404); exit; }
// Build target URL on the gitea instance
$url = rtrim($row['base_url'], '/') . '/' . ltrim($path, '/');
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_TIMEOUT => 30,
CURLOPT_SSL_VERIFYPEER => true,
CURLOPT_SSL_VERIFYHOST => 2,
CURLOPT_HTTPHEADER => [
'Authorization: token ' . giteaDecrypt($row['token']),
'User-Agent: hyprcrd-gitea-plugin/1.0',
],
]);
$body = curl_exec($ch);
if ($body === false) {
$err = curl_error($ch);
curl_close($ch);
http_response_code(502);
header('Content-Type: text/plain');
echo "Could not reach gitea: $err";
exit;
}
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$contentType = curl_getinfo($ch, CURLINFO_CONTENT_TYPE) ?: 'application/octet-stream';
curl_close($ch);
if ($code >= 400) {
http_response_code($code);
header('Content-Type: text/plain');
echo "Gitea returned HTTP $code";
exit;
}
// Strip any default headers PHP might emit so we can replace them
header_remove('Pragma');
header_remove('Expires');
header('Content-Type: ' . $contentType);
header('Cache-Control: private, max-age=3600');
header('Content-Length: ' . strlen($body));
echo $body;
exit;
}
// ── Create issue ──
if (preg_match('#^/api/gitea/repo/(\d+)/([^/]+)/([^/]+)/issues$#', $route, $m) && $method === 'POST') {
$accId = (int)$m[1]; $owner = urldecode($m[2]); $repo = urldecode($m[3]);
$row = giteaGetAccount($accId);
if (!$row) giteaJson(['error' => 'account not found'], 404);
$input = json_decode(file_get_contents('php://input'), true) ?: [];
$title = trim((string)($input['title'] ?? ''));
if ($title === '') giteaJson(['error' => 'title is required'], 400);
$opts = ['title' => $title];
if (array_key_exists('body', $input)) $opts['body'] = (string)$input['body'];
if (array_key_exists('labels', $input) && is_array($input['labels'])) {
$opts['labels'] = array_map('intval', $input['labels']);
}
if (array_key_exists('assignees', $input) && is_array($input['assignees'])) {
$opts['assignees'] = array_values(array_filter(array_map('strval', $input['assignees'])));
}
if (!empty($input['milestone'])) $opts['milestone'] = (int)$input['milestone'];
if (!empty($input['due_date'])) $opts['due_date'] = (string)$input['due_date'];
try {
$client = giteaOpenClient($row);
$created = $client->createIssue($owner, $repo, $opts);
giteaJson([
'ok' => true,
'issue' => [
'id' => $created['id'] ?? null,
'number' => $created['number'] ?? null,
'title' => $created['title'] ?? '',
'state' => $created['state'] ?? 'open',
'html_url' => $created['html_url'] ?? null,
'owner' => $owner,
'repo_name' => $repo,
],
]);
} catch (GiteaApiException $e) {
giteaJson(['ok' => false, 'error' => "HTTP {$e->statusCode}: " . substr($e->body, 0, 300)], 200);
} catch (\Throwable $e) {
giteaJson(['ok' => false, 'error' => $e->getMessage()], 200);
}
}
// ── User repos (for the repo picker) ──
if ($route === '/api/gitea/repos' && $method === 'GET') {
$accId = (int)($_GET['account'] ?? 0);
$row = giteaGetAccount($accId);
if (!$row) giteaJson(['error' => 'account not found'], 404);
try {
$client = giteaOpenClient($row);
$repos = $client->listMyRepos(1, 100);
$mapped = array_map(fn($r) => [
'id' => $r['id'] ?? null,
'full_name' => $r['full_name'] ?? null,
'name' => $r['name'] ?? null,
'owner' => $r['owner']['login'] ?? null,
'description' => $r['description'] ?? '',
'private' => $r['private'] ?? false,
'archived' => $r['archived'] ?? false,
'permissions' => $r['permissions'] ?? null,
], $repos);
// Filter out archived repos and ones without push permission
$mapped = array_values(array_filter($mapped, function ($r) {
if ($r['archived']) return false;
$p = $r['permissions'] ?? null;
if ($p && isset($p['push'])) return $p['push'] || $p['admin'] ?? false;
return true; // unknown permissions = include
}));
giteaJson(['ok' => true, 'repos' => $mapped]);
} catch (GiteaApiException $e) {
giteaJson(['ok' => false, 'error' => "HTTP {$e->statusCode}: " . substr($e->body, 0, 300)], 200);
} catch (\Throwable $e) {
giteaJson(['ok' => false, 'error' => $e->getMessage()], 200);
}
}
// ── Repo metadata ──
if (preg_match('#^/api/gitea/repo/(\d+)/([^/]+)/([^/]+)/labels$#', $route, $m) && $method === 'GET') {
$accId = (int)$m[1]; $owner = urldecode($m[2]); $repo = urldecode($m[3]);
$row = giteaGetAccount($accId);
if (!$row) giteaJson(['error' => 'account not found'], 404);
try {
$client = giteaOpenClient($row);
// Merge repo-level + org-level labels (org labels are inherited by
// repos in an org but don't appear in /repos/.../labels)
$repoLabels = $client->listRepoLabels($owner, $repo);
$orgLabels = [];
$warning = null;
try {
$orgLabels = $client->listOrgLabels($owner);
} catch (GiteaApiException $e) {
if ($e->statusCode === 404) {
// Owner is a user, not an org — no org labels exist. silent.
} elseif ($e->statusCode === 403) {
// Token lacks read:organization scope — surface a warning
// but don't fail the whole request; repo labels still work.
$warning = 'Token lacks read:organization scope — org-level labels are not visible. Update your gitea token to include read:organization to see them.';
} else {
throw $e;
}
}
// Dedupe by id (in case the same label appears in both)
$seen = [];
$merged = [];
foreach (array_merge($repoLabels, $orgLabels) as $l) {
$id = $l['id'] ?? null;
if ($id !== null && !isset($seen[$id])) {
$seen[$id] = true;
$merged[] = $l;
}
}
$payload = [
'ok' => true,
'labels' => array_map(fn($l) => [
'id' => $l['id'] ?? null,
'name' => $l['name'] ?? '',
'color' => $l['color'] ?? '888888',
'description' => $l['description'] ?? '',
], $merged),
];
if ($warning) $payload['warning'] = $warning;
giteaJson($payload);
} catch (GiteaApiException $e) {
giteaJson(['ok' => false, 'error' => "HTTP {$e->statusCode}: " . substr($e->body, 0, 300)], 200);
} catch (\Throwable $e) {
giteaJson(['ok' => false, 'error' => $e->getMessage()], 200);
}
}
if (preg_match('#^/api/gitea/repo/(\d+)/([^/]+)/([^/]+)/milestones$#', $route, $m) && $method === 'GET') {
$accId = (int)$m[1]; $owner = urldecode($m[2]); $repo = urldecode($m[3]);
$row = giteaGetAccount($accId);
if (!$row) giteaJson(['error' => 'account not found'], 404);
try {
$client = giteaOpenClient($row);
$milestones = $client->listRepoMilestones($owner, $repo, 'open');
giteaJson([
'ok' => true,
'milestones' => array_map(fn($ms) => [
'id' => $ms['id'] ?? null,
'title' => $ms['title'] ?? '',
'description' => $ms['description'] ?? '',
'due_on' => $ms['due_on'] ?? null,
'state' => $ms['state'] ?? 'open',
], $milestones),
]);
} catch (GiteaApiException $e) {
giteaJson(['ok' => false, 'error' => "HTTP {$e->statusCode}: " . substr($e->body, 0, 300)], 200);
} catch (\Throwable $e) {
giteaJson(['ok' => false, 'error' => $e->getMessage()], 200);
}
}
if (preg_match('#^/api/gitea/repo/(\d+)/([^/]+)/([^/]+)/collaborators$#', $route, $m) && $method === 'GET') {
$accId = (int)$m[1]; $owner = urldecode($m[2]); $repo = urldecode($m[3]);
$row = giteaGetAccount($accId);
if (!$row) giteaJson(['error' => 'account not found'], 404);
try {
$client = giteaOpenClient($row);
// Use /assignees instead of /collaborators — it returns the full
// set of users that can be assigned (owner + org members +
// explicit collaborators), which is what we actually want.
$assignees = $client->listRepoAssignees($owner, $repo);
giteaJson([
'ok' => true,
'collaborators' => array_map(fn($u) => [
'login' => $u['login'] ?? '',
'full_name' => $u['full_name'] ?? '',
'avatar_url' => $u['avatar_url'] ?? '',
], $assignees),
]);
} catch (GiteaApiException $e) {
giteaJson(['ok' => false, 'error' => "HTTP {$e->statusCode}: " . substr($e->body, 0, 300)], 200);
} catch (\Throwable $e) {
giteaJson(['ok' => false, 'error' => $e->getMessage()], 200);
}
}
if (preg_match('#^/api/gitea/accounts/(\d+)/test$#', $route, $m) && $method === 'POST') {
$id = (int)$m[1];
$row = giteaGetAccount($id);
if (!$row) giteaJson(['error' => 'account not found'], 404);
try {
$client = giteaOpenClient($row);
$user = $client->testConnection();
giteaJson([
'ok' => true,
'user' => [
'login' => $user['login'] ?? null,
'full_name' => $user['full_name'] ?? null,
'email' => $user['email'] ?? null,
'avatar_url' => $user['avatar_url'] ?? null,
],
'base_url' => $row['base_url'],
]);
} catch (GiteaApiException $e) {
giteaJson(['ok' => false, 'error' => "HTTP {$e->statusCode}: " . substr($e->body, 0, 300)], 200);
} catch (\Throwable $e) {
giteaJson(['ok' => false, 'error' => $e->getMessage()], 200);
}
}
});