Bluesky Posts mit Linkvorschau via Dart veröffentlichen
In den letzten Monaten habe ich Dart als Lösung für serverseitige Implementierungen für mich entdeckt. Sowohl das Backend meines Blogs, wie auch verschiedene andere Server-Tools habe ich mittlerweile darauf umgestellt. Darunter ist auch mein Closed-Source Tool BoehrsiTweetHub, welches initial Daten mit Twitter / X geteilt hat und vor kurzem für Bluesky erweitert wurde.
Das Teilen von Blog Inhalten oder anderem Content mit Twitter / X ist relativ simpel, da dort quasi alle Informationen von der Plattform selbst gezogen werden. Dies ist bei Bluesky etwas anders, hier muss man den Content einer Card und vor allem Medieninhalte gesondert hochladen.
Das ist alles mit den aktuell vorhandenen Bibliotheken lösbar, bedarf allerdings etwas Wissen. Da ich kein komplettes Beispiel dafür finden konnte, musste ich mir einiges herleiten und anlesen, was ich euch ersparen möchte. Entsprechend findet ihr im unteren Teil des Beitrags meine Implementierung, um Webseiten bzw. Links inklusive Titel, Beschreibungstext und Bild auf Bluesky zu veröffentlichen.
Um euch die Nutzung einfacher zu machen findet ihr den kompletten Code aus lauffähigen Github Gist in den Related Links.
Vorab sind die zu nutzenden Bibliotheken zu erwähnen, welche ich im folgenden Beispiel genutzt habe. Alle Bibliotheken sind dabei Dart-Only, sodass Flutter nicht benötigt wird.
Da ich einige Probleme mit der Kompatibilität von Bibliotheken hatte hier einmal die exakten Versionen die ich in meiner pubspec.yaml
nutze.
bluesky: ^0.18.1
bluesky_text: ^0.7.1
http: ^1.2.2
metadata_fetch: ^0.4.2
Die fetchDataAndUploadMedia
Methode holt wie der Name suggeriert alle relevanten Daten für eine Webseite bzw. einen Link und lädt das erhaltene Thumbnail auf Bluesky hoch. Für den Upload der Medieninhalte wird die fetchAndUploadImage
Methode genutzt.
Future<Embed?> fetchDataAndUploadMedia(Bluesky bluesky, String url) async {
// Fetch / extract web content
final data = await MetadataFetch.extract(url);
final title = data?.title;
final description = data?.description;
// Upload media to Bluesky
Blob? blob = await fetchAndUploadImage(bluesky, data?.image);
// If everything was fetched and the media upload was successful, return the formatted data
if (title != null && description != null && blob != null) {
return Embed.external(
data: EmbedExternal(
external: EmbedExternalThumbnail(
uri: url,
title: title,
description: description,
blob: blob,
),
),
);
}
// If something went wrong, return null
return null;
}
Für den Upload wird das Bild zuerst von der angegebenen Webseite heruntergeladen und anschließend über die Bluesky Bibliothek hochgeladen.
Future<Blob?> fetchAndUploadImage(Bluesky bluesky, String? imageUrl) async {
if (imageUrl != null) {
try {
// Fetch the image from the web
final uri = Uri.parse(imageUrl);
final http.Response response = await http.get(uri);
// Upload the image to Bluesky and return the blog, which is needed to create the post
final uploadResponse = await bluesky.atproto.repo.uploadBlob(response.bodyBytes);
return uploadResponse.data.blob;
} catch (e) {
print(e);
}
}
// If something went wrong, return null
return null;
}
Um das herumreichen der Daten zu vereinfachen habe ich mir eine simple Datenklasse erstellt, welche die URL und den Text, welcher im Bluesky Post stehen soll, enthält.
class PostDataEntry {
final String url;
final String text;
PostDataEntry({required this.url, required this.text});
}
Die postEntry
Methode nutzt nun die zuvor definierten Methoden, um alles zu verbinden und den Post zu erstellen. Dabei laden wir alle Inhalte für die angegebene Webseite bzw. den Link, laden das Bild bei Bluesky hoch, erstellen daraus eine Card und veröffentlichen diesen Post, bestückt mit dem von uns definierten Text auf Bluesky.
Future<void> postEntry(PostDataEntry postData, String username, String password) async {
try {
// Create the Bluesky session. As the login is done by username and password it is
// suggested to create an app-specifc password, to avoid using your normal credentials
final session = await createSession(
identifier: username,
password: password,
);
// Create the actual Bluesky object from the session
final bluesky = Bluesky.fromSession(
await session.data,
);
// Format the text to make links and tags clickable within your post
final blueskyText = BlueskyText(postData.text);
final facets = await blueskyText.entities.toFacets();
// Post the content, including the card and your defined text
final entry = await bluesky.feed.post(
text: blueskyText.value,
facets: facets.map(Facet.fromJson).toList(),
embed: await fetchDataAndUploadMedia(bluesky, postData.url),
);
print(entry);
} on UnauthorizedException catch (e) {
print(e);
} on XRPCException catch (e) {
print(e);
}
}