P6: link previews read by the server, as the owner decided
- A public post that links to a page gets a preview card: from the post's own data first (FEP-8967 Link preview, the object's image, title and summary); otherwise the server reads the page once, 0-60 s after the post arrives, never when someone reads it. OpenGraph and Twitter tags give title, description and image; one LinkPreview per address is cached for 7 days and shared by the whole server, so a fetch never points at a persona. - Lemmy link posts, which carry no title or description, get them filled in. - The page fetch uses the guarded client (public addresses, three redirects, HTML only, first 512 KB). - Federation:FetchLinkPreviews switches page fetching off. - Local public posts get cards too. Checked live: a link to a GoToSocial profile page becomes a card with its title, description and proxied image. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012CzABvBkbcFqoHdmi8b9WB
This commit is contained in:
1 parent
fdcf5176bc
commit
001fdac3be
17 files changed
+314
-13
No files matched your search
@@ -22,6 +22,7 @@ namespace PrivaPub.Infrastructure.Http
|
||||
bool IsAllowed(Uri target);
|
||||
Task<FetchedJson> GetJson(string url, string accept, Action<HttpRequestMessage> sign, CancellationToken token);
|
||||
bool FailedTemporarily(string url);
|
||||
Task<(Uri FinalUri, string Html)> GetPage(string url, CancellationToken token);
|
||||
Task<HttpResponseMessage> Send(HttpRequestMessage request, CancellationToken token);
|
||||
Task<(byte[] Bytes, string ContentType)> GetMedia(string url, long maxBytes, CancellationToken token);
|
||||
}
|
||||
@@ -181,6 +182,66 @@ namespace PrivaPub.Infrastructure.Http
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<(Uri FinalUri, string Html)> GetPage(string url, CancellationToken token)
|
||||
{
|
||||
if (!Uri.TryCreate(url, UriKind.Absolute, out var target) || !IsAllowed(target))
|
||||
return default;
|
||||
using var timeout = CancellationTokenSource.CreateLinkedTokenSource(token);
|
||||
timeout.CancelAfter(RequestTimeout);
|
||||
try
|
||||
{
|
||||
for (var hop = 0; hop <= MaxRedirects; hop++)
|
||||
{
|
||||
using var request = new HttpRequestMessage(HttpMethod.Get, target);
|
||||
request.Headers.Accept.ParseAdd("text/html, application/xhtml+xml");
|
||||
using var response = await _httpClientFactory.CreateClient(ClientName).SendAsync(request, HttpCompletionOption.ResponseHeadersRead, timeout.Token);
|
||||
if (IsRedirect(response.StatusCode))
|
||||
{
|
||||
var location = response.Headers.Location;
|
||||
var next = location == default ? default : location.IsAbsoluteUri ? location : new Uri(target, location);
|
||||
if (!IsAllowed(next))
|
||||
return default;
|
||||
target = next;
|
||||
continue;
|
||||
}
|
||||
var mediaType = response.Content.Headers.ContentType?.MediaType?.ToLowerInvariant();
|
||||
if (!response.IsSuccessStatusCode || mediaType is not ("text/html" or "application/xhtml+xml"))
|
||||
return default;
|
||||
var bytes = await ReadPrefix(response.Content, MaxPageBytes, timeout.Token);
|
||||
var charset = response.Content.Headers.ContentType?.CharSet?.Trim('"');
|
||||
var encoding = System.Text.Encoding.UTF8;
|
||||
try
|
||||
{
|
||||
if (!string.IsNullOrEmpty(charset))
|
||||
encoding = System.Text.Encoding.GetEncoding(charset);
|
||||
}
|
||||
catch (ArgumentException)
|
||||
{
|
||||
}
|
||||
return (target, encoding.GetString(bytes));
|
||||
}
|
||||
return default;
|
||||
}
|
||||
catch (Exception ex) when (ex is HttpRequestException or BlockedDestinationException or OperationCanceledException && !token.IsCancellationRequested)
|
||||
{
|
||||
_logger.LogInformation("Page {Url} refused: {Reason}", url, ex.Message);
|
||||
return default;
|
||||
}
|
||||
}
|
||||
|
||||
const int MaxPageBytes = 512 * 1024;
|
||||
|
||||
static async Task<byte[]> ReadPrefix(HttpContent content, int limit, CancellationToken token)
|
||||
{
|
||||
await using var stream = await content.ReadAsStreamAsync(token);
|
||||
var buffer = new byte[limit];
|
||||
var total = 0;
|
||||
int read;
|
||||
while (total < limit && (read = await stream.ReadAsync(buffer.AsMemory(total, limit - total), token)) > 0)
|
||||
total += read;
|
||||
return buffer[..total];
|
||||
}
|
||||
|
||||
public async Task<HttpResponseMessage> Send(HttpRequestMessage request, CancellationToken token)
|
||||
{
|
||||
if (!IsAllowed(request.RequestUri))
|
||||
|
||||
@@ -6,5 +6,6 @@ namespace PrivaPub.Infrastructure.Http
|
||||
public bool AllowPlainHttp { get; set; }
|
||||
public bool SecureMode { get; set; }
|
||||
public bool AcceptAnyCertificate { get; set; }
|
||||
public bool FetchLinkPreviews { get; set; } = true;//owner decision 1: the server reads linked pages of public posts
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user