drasticactions / FishyFlip

Fishyflip - a .NET ATProtocol/Bluesky Library
MIT License
62 stars 7 forks source link

[Question] Embed link to news article to show Open Graph (OG) parameters #89

Open JeepNL opened 1 week ago

JeepNL commented 1 week ago

I've seen this: https://github.com/drasticactions/FishyFlip/issues/68#issuecomment-2443891033 and it's great!

What I would like to do is to embed a link in the post, so it shows the Open Graph (OG) parameters (like an image and text from the linked article) and I think I need to use Embed? embed=null. for it in (after facets):

CreatePostResponse? result = (await atProtocol.Repo.CreatePostAsync(postText, facets)).HandleResult();

but I don't know how to use Embed. The published post will show the post's text but not the link in the text, but it's shows the embedded link below the text like this:

Screenshot 2024-11-14 191812

Any help is very much appreciated :)

JeepNL commented 1 week ago

Oh, wait ... The Bluesky instance doesn't get the OG params itself by just adding an 'embed link'? That's what Twitter and Mastodon do, but I now see in the code of X.Bluesky GitHub repo this (EmbedCardBuilder):


using System.Net.Http.Headers;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using X.Bluesky.Models;

namespace X.Bluesky;

public class EmbedCardBuilder
{
    private readonly ILogger _logger;
    private readonly FileTypeHelper _fileTypeHelper;
    private readonly IHttpClientFactory _httpClientFactory;
    private readonly Session _session;

    public EmbedCardBuilder(IHttpClientFactory httpClientFactory, Session session, ILogger logger)
    {
        _logger = logger;
        _httpClientFactory = httpClientFactory;
        _session = session;
        _fileTypeHelper = new FileTypeHelper(logger);
    }

    /// <summary>
    /// Create embed card
    /// </summary>
    /// <param name="url"></param>
    /// <returns></returns>
    public async Task<EmbedCard> GetEmbedCard(Uri url)
    {
        var extractor = new Web.MetaExtractor.Extractor();
        var metadata = await extractor.ExtractAsync(url);

        var card = new EmbedCard
        {
            Uri = url.ToString(),
            Title = metadata.Title,
            Description = metadata.Description
        };

        if (metadata.Images.Any())
        {
            var imgUrl = metadata.Images.FirstOrDefault();

            if (!string.IsNullOrWhiteSpace(imgUrl))
            {
                if (!imgUrl.Contains("://"))
                {
                    card.Thumb = await UploadImageAndSetThumbAsync(new Uri(url, imgUrl));
                }
                else
                {
                    card.Thumb = await UploadImageAndSetThumbAsync(new Uri(imgUrl));    
                }

                _logger.LogInformation("EmbedCard created");
            }
        }

        return card;
    }

    private async Task<Thumb?> UploadImageAndSetThumbAsync(Uri imageUrl)
    {
        var httpClient = _httpClientFactory.CreateClient();

        var imgResp = await httpClient.GetAsync(imageUrl);
        imgResp.EnsureSuccessStatusCode();

        var mimeType = _fileTypeHelper.GetMimeTypeFromUrl(imageUrl);

        var imageContent = new StreamContent(await imgResp.Content.ReadAsStreamAsync());
        imageContent.Headers.ContentType = new MediaTypeHeaderValue(mimeType);

        var request = new HttpRequestMessage(HttpMethod.Post, "https://bsky.social/xrpc/com.atproto.repo.uploadBlob")
        {
            Content = imageContent,
        };

        // Add the Authorization header with the access token to the request message
        request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", _session.AccessJwt);

        var response = await httpClient.SendAsync(request);

        response.EnsureSuccessStatusCode();

        var json = await response.Content.ReadAsStringAsync();
        var blob = JsonConvert.DeserializeObject<BlobResponse>(json);

        var card = blob?.Blob;

        if (card != null)
        {
            // ToDo: fix it
            // This is hack for fix problem when Type is empty after deserialization
            card.Type = "blob"; 
        }

        return card;
    }
}
JeepNL commented 1 week ago

Okay, I need to fetch the OG params myself .... hmm

https://github.com/bluesky-social/atproto/discussions/1304

drasticactions commented 1 week ago

I originally had a sample application for CLI access that had an implementation of "OpenGraph" support. The idea is to create an ExternalEmbed where you fill in the details and pass that in as an Embed when making the post. Bluesky itself doesn't process OpenGraph URLs itself, you have to supply the parameters.

https://github.com/drasticactions/FishyFlip/blob/v1.7/apps/bskycli/Program.cs#L462-L560

This code should still work with the newest versions and should give an idea for how to do it. I could probably clean it up and bake it into FishyFlip too as a helper.

JeepNL commented 1 week ago

Thank you, that's great and very informative! I wil use this. I'm running a .NET/C# console worker service on my Ubuntu VPS which publishes new posts to Bluesky (so it's a news bot). Because of this, I've to look out for HttpClient Socket Exhaustion see: https://learn.microsoft.com/en-us/dotnet/fundamentals/networking/http/httpclient-guidelines#recommended-use, so not use a new HttpClient for every request, but re-use an existing HttpClient. If you are going to create a helper, I think this could be important too?

Something else: maybe it's an idea to add GitHub Discussion here, so devs can ask questions, share code etc? See: https://github.com/features/discussions

drasticactions commented 1 week ago

For the code above, as it was used in a CLI app where it was for a single instance of getting the values before quitting, it was fine. Of course, moving it into the library, I would change it so you bring in your HttpContext (Hence, "Cleaning it up" 😉)

If nothing else, I should document it for the ways to do. I had started writing docs for the common "Here's how to do (feature)" stuff as a quick start, and this would fall into that too.

As for Discussions, IMO I don't think this gets high enough traffic at the moment to need that. I'm fine with this being a place for questions where once it's answered (or once we've created a solution like making a helper, docs, etc) can close it. I turn off notifications and check in on issues periodically so I can maintain a healthy balance for my OSS projects.

I'm also on the Bluesky API Touchers discord, which is also a good place for general ATProtocol questions.

JeepNL commented 3 days ago

I understand that a Discussions section isn't really necessary at this point. I just felt a bit uncomfortable asking questions in 'issues,' but as long as you’re okay with it, there’s no problem.

I believe that including short CLI code samples would be helpful. For Twitter, I used LinqToTwitter, and Joe Mayo published a sample console program in his repository that clarified a lot about how to use the library. I found it very useful.

If you want you can close this issue. Thank you for your replies!