You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

238 lines
6.7 KiB

<?php
declare(strict_types=1);
namespace App\Service;
/**
* Website and NIP-05 links from kind-0 JSON and kind-0 tags, normalized and deduplicated.
*/
final class ProfileIdentityLinksBuilder
{
/**
* @param list<list<string>> $kind0Tags
*
* @return list<array{label: string, href: string}>
*/
public function buildWebsites(object $content, array $kind0Tags): array
{
$raw = [];
foreach ($this->stringsFromJsonField($content, 'website') as $s) {
$raw[] = $s;
}
foreach ($this->stringsFromJsonField($content, 'websites') as $s) {
$raw[] = $s;
}
foreach (self::tagValuesForNames($kind0Tags, ['url', 'website', 'web']) as $s) {
$raw[] = $s;
}
$out = [];
$seen = [];
foreach ($raw as $u) {
$u = trim($u);
if ($u === '') {
continue;
}
$href = $this->normalizeHttpUrl($u);
if ($href === null) {
continue;
}
$k = self::urlDedupKey($href);
if (isset($seen[$k])) {
continue;
}
$seen[$k] = true;
$out[] = [
'label' => $this->displayLabelForHttpUrl($href),
'href' => $href,
];
}
usort($out, static fn (array $a, array $b): int => strcasecmp($a['label'], $b['label']));
return $out;
}
/**
* @param list<list<string>> $kind0Tags
*
* @return list<array{label: string, href: string}>
*/
public function buildNip05(object $content, array $kind0Tags): array
{
$raw = [];
foreach ($this->stringsFromJsonField($content, 'nip05') as $s) {
$raw[] = $s;
}
foreach (self::tagValuesForNames($kind0Tags, ['nip05']) as $s) {
$raw[] = $s;
}
$out = [];
$seen = [];
foreach ($raw as $id) {
$id = trim(strtolower($id));
if ($id === '' || !str_contains($id, '@')) {
continue;
}
if (isset($seen[$id])) {
continue;
}
$seen[$id] = true;
$parts = explode('@', $id, 2);
$local = $parts[0];
$domain = $parts[1];
if ($local === '' || $domain === '' || str_contains($domain, ' ')) {
continue;
}
$href = 'https://'.$domain.'/.well-known/nostr.json?name='.rawurlencode($local);
$out[] = [
'label' => $id,
'href' => $href,
];
}
usort($out, static fn (array $a, array $b): int => strcasecmp($a['label'], $b['label']));
return $out;
}
/**
* Adds a site-assigned NIP-05 (e.g. under the blog domain) into the same list as profile NIP-05,
* with the same link shape as {@see buildNip05}, deduped by label.
*
* @param list<array{label: string, href: string}> $rows
*
* @return list<array{label: string, href: string}>
*/
public function mergeSiteNip05IntoList(array $rows, string $siteNip05): array
{
$siteNip05 = trim(strtolower($siteNip05));
if ($siteNip05 === '' || !str_contains($siteNip05, '@')) {
return $rows;
}
$seen = [];
foreach ($rows as $r) {
$seen[strtolower((string) $r['label'])] = true;
}
if (isset($seen[$siteNip05])) {
return $rows;
}
$parts = explode('@', $siteNip05, 2);
$local = $parts[0];
$domain = $parts[1];
if ($local === '' || $domain === '' || str_contains($domain, ' ')) {
return $rows;
}
$href = 'https://'.$domain.'/.well-known/nostr.json?name='.rawurlencode($local);
$rows[] = [
'label' => $siteNip05,
'href' => $href,
];
usort($rows, static fn (array $a, array $b): int => strcasecmp($a['label'], $b['label']));
return $rows;
}
/**
* @return list<string>
*/
private function stringsFromJsonField(object $o, string $key): array
{
if (!isset($o->{$key})) {
return [];
}
$v = $o->{$key};
if (\is_string($v)) {
return [trim($v)];
}
if (!\is_array($v)) {
return [];
}
$out = [];
foreach ($v as $x) {
if (\is_string($x) && trim($x) !== '') {
$out[] = trim($x);
}
}
return $out;
}
/**
* @param list<list<string>> $tags
* @param list<string> $tagNames lowercased
* @return list<string>
*/
private static function tagValuesForNames(array $tags, array $tagNames): array
{
$want = array_fill_keys(array_map(static fn (string $s): string => strtolower($s), $tagNames), true);
$out = [];
foreach ($tags as $t) {
if (!isset($t[0], $t[1])) {
continue;
}
$name = strtolower((string) $t[0]);
if (!isset($want[$name])) {
continue;
}
$v = trim((string) $t[1]);
if ($v !== '') {
$out[] = $v;
}
}
return $out;
}
private function normalizeHttpUrl(string $url): ?string
{
$u = trim($url);
if ($u === '') {
return null;
}
if (!preg_match('#^[a-zA-Z][a-zA-Z0-9+.-]*:#', $u)) {
$u = 'https://'.$u;
}
if (!str_starts_with(strtolower($u), 'http://') && !str_starts_with(strtolower($u), 'https://')) {
return null;
}
return $u;
}
private static function urlDedupKey(string $href): string
{
$p = @parse_url($href);
if (!\is_array($p)) {
return strtolower($href);
}
$host = strtolower((string) ($p['host'] ?? ''));
$path = (string) ($p['path'] ?? '');
if ($path !== '/' && str_ends_with($path, '/')) {
$path = rtrim($path, '/');
}
return $host.$path.($p['query'] ?? '');
}
private function displayLabelForHttpUrl(string $href): string
{
$p = @parse_url($href);
if (\is_array($p) && !empty($p['host'])) {
$host = (string) $p['host'];
$path = (string) ($p['path'] ?? '');
if ($path === '' || $path === '/') {
return $host;
}
if (strlen($path) > 32) {
return $host.$path;
}
return $host.$path;
}
if (strlen($href) > 48) {
return substr($href, 0, 32).'…';
}
return $href;
}
}