DEV Community

Shinya Kato
Shinya Kato

Posted on • Updated on

Let's Post to Bluesky Social easily with Dart and Flutter

As already noted by many media articles, Bluesky Social is a decentralized social networking service with a Twitter-like UI. Bluesky Social is built on a next-generation common infrastructure called the AT Protocol, and the road ahead is very ambitious and full of unknown possibilities.

This article mainly shows how to post to Bluesky Social from the API in Dart language. I will not actively present the implementation in Flutter, but you can apply the same procedure in Flutter by reading this article.

What Is Dart?

For those who are not familiar with the Dart language, Dart is a relatively new programming language developed by Google. Dart has a syntax similar to Java and C, but with modern language specifications such as null safety. Above all, Flutter is probably the reason Dart is getting the most attention.

Dart programming language | Dart

Dart is an approachable, portable, and productive language for high-quality apps on any platform.

favicon dart.dev

Flutter - Build apps for any screen

Flutter transforms the entire app development process. Build, test, and deploy beautiful mobile, web, desktop, and embedded apps from a single codebase.

favicon flutter.dev

There is already a powerful package that makes it easy to use Bluesky Social's API with Dart and Flutter, and this article will present sample code using that package.

Install Package

To easily use Bluesky Social's API in Dart or Flutter, use the package bluesky. I develop and maintain this package and it already supports almost all endpoints, is well tested and very stable.

It is already being used in several third-party apps, and I highly recommend this package for your Bluesky Social-related apps in Dart or Flutter.

bluesky | Dart package

The most famous and powerful Dart/Flutter library for Bluesky Social.

favicon pub.dev

Or you can see more details in in following official page.

AT Protocol and Bluesky Social Things for Dart and Flutter | atproto.dart

Powerful suite of AT Protocol and Bluesky-related packages for Dart/Flutter

favicon atprotodart.com

Well, let's install this package with following commands.

With Dart:

dart pub add bluesky
Enter fullscreen mode Exit fullscreen mode
dart pub get
Enter fullscreen mode Exit fullscreen mode

With Flutter:

flutter pub add bluesky
Enter fullscreen mode Exit fullscreen mode
flutter pub get
Enter fullscreen mode Exit fullscreen mode

And let's check your pubspec.yaml in your Dart or Flutter app. It is successful if it looks like this and this article uses bluesky v0.6.0.

name: bluesky_post
description: A sample command-line application.
version: 1.0.0
environment:
  sdk: ^3.0.0

dependencies:
  bluesky: ^0.6.0

dev_dependencies:
  lints: ^2.0.0
  test: ^1.21.0
Enter fullscreen mode Exit fullscreen mode

Let's Post!

The easiest way to post to Bluesky Social using the bluesky package is as follows. The following example simply posts any text.

import 'package:bluesky/bluesky.dart' as bsky;

Future<void> main() async {
  //! You need to be authenticated by credentials.
  final session = await bsky.createSession(
    identifier: 'HANDLE_OR_EMAIL',
    password: 'PASSWORD_OR_APP_PASSWORD',
  );

  //! Create Bluesky from session data.
  final bluesky = bsky.Bluesky.fromSession(session.data);

  final ref = await bluesky.feeds.createPost(
    text: 'Hello, Bluesky!',
  );

  //! StrongRef
  print(ref.data);
}
Enter fullscreen mode Exit fullscreen mode

I previously wrote an article about using the Firehose API with the bluesky package, which did not require credentials. However, the endpoint you will be using to post to Bluesky Social will always require your credentials.

For example, the above example uses the createSession method to authenticate with the bsky.social server using your credentials. If the authentication fails, an error is returned by bsky.social.

As a side note, if you have an account on a server other than bsky.social, you can specify any server as follows:

final session = await bsky.createSession(
  service: 'stem.social',
  identifier: 'HANDLE_OR_EMAIL',
  password: 'PASSWORD_OR_APP_PASSWORD',
);
Enter fullscreen mode Exit fullscreen mode

Then, an instance of the Bluesky object, which is required to use the bluesky package, is created using the .fromSession constructor. The .fromSession constructor can be used to create a Bluesky object from a Session object without over or under creating it.

final bluesky = bsky.Bluesky.fromSession(session.data);
Enter fullscreen mode Exit fullscreen mode

Okay now, if you could get session and Bluesky object from session, all you need to do is post any text with the following code.

final ref = await bluesky.feeds.createPost(
  text: 'Hello, Bluesky!',
);
Enter fullscreen mode Exit fullscreen mode

With just this, you can send any post to Bluesky Social! The data returned from the createPost method is a reference to the actual data generated. It is represented on the AT Protocol as StrongRef and consists of an AT URI and CID link to the generated data.

Also, although not in the above code, you can delete a generated Post or other record from the AT URI as follows.

//! Delete any records.
await bluesky.repositories.deleteRecord(uri: ref.data.uri);
Enter fullscreen mode Exit fullscreen mode

Post with Images

Now that you know how to use the bluesky package to post arbitrary text to Bluesky Social, it would be useful to be able to attach images and such when actually using it.

Basically, in order to attach an image to a particular piece of content, you must first upload that image data and then POST the uploaded data in Multipart format. However, with the bluesky package, you don't have to be aware of these difficult processes at all.

Well, you can send a post with an image by doing the following:

import 'dart:io';

import 'package:bluesky/bluesky.dart' as bsky;

Future<void> main() async {
  final session = await bsky.createSession(
    identifier: 'HANDLE_OR_EMAIL',
    password: 'PASSWORD_OR_APP_PASSWORD',
  );

  final bluesky = bsky.Bluesky.fromSession(session.data);

  //! Upload photo!
  final uploaded = await bluesky.repositories.uploadBlob(
    File('./funny_photo.jpg').readAsBytesSync(), // File bytes
  );

  final ref = await bluesky.feeds.createPost(
    text: 'Hello, Bluesky!',

    //! Add it.
    embed: bsky.Embed.images(
      data: bsky.EmbedImages(
        images: [
          bsky.Image(
            alt: 'This is my funny photo!',
            image: uploaded.data.blob,
          ),
        ],
      ),
    ),
  );

  print(ref);
}
Enter fullscreen mode Exit fullscreen mode

The above example shows that it is very easy to send a post with an image.

All you have to do is upload an arbitrary image file using the uploadBlob method, and pass the uploaded Blob data to the embed parameter of createPost.

Quote the Post

You can also quote a post that has already been submitted easily as follows.

import 'package:bluesky/bluesky.dart' as bsky;

Future<void> main() async {
  final session = await bsky.createSession(
    identifier: 'HANDLE_OR_EMAIL',
    password: 'PASSWORD_OR_APP_PASSWORD',
  );

  final bluesky = bsky.Bluesky.fromSession(session.data);

  //! Let's use this post as quote.
  final ref = await bluesky.feeds.createPost(text: 'This will be quoted.');

  await bluesky.feeds.createPost(
    text: 'Hello, Bluesky!',

    //! Add it.
    embed: bsky.Embed.record(
      data: bsky.EmbedRecord(ref: ref.data),
    ),
  );
}
Enter fullscreen mode Exit fullscreen mode

Attach a Link Card to an External Site

When using Bluesky's API to embed a post with a link card from an external site, you will need to configure that link card yourself. The bluesky package makes it easy to set up.

import 'package:bluesky/bluesky.dart' as bsky;

Future<void> main() async {
  final session = await bsky.createSession(
    identifier: 'HANDLE_OR_EMAIL',
    password: 'PASSWORD_OR_APP_PASSWORD',
  );

  final bluesky = bsky.Bluesky.fromSession(session.data);

  await bluesky.feeds.createPost(
    text: 'Hello, Bluesky!',

    //! Add it.
    embed: bsky.Embed.external(
      data: bsky.EmbedExternal(
        external: bsky.EmbedExternalThumbnail(
          uri: 'https://shinyakato.dev',
          title: 'Shinya Kato',
          description: 'This is my site!',
        ),
      ),
    ),
  );
}
Enter fullscreen mode Exit fullscreen mode

Reply to a Specific Post

Replying to a specific post is a basic feature. Using the bluesky package, you can reply as follows.

import 'package:bluesky/bluesky.dart' as bsky;

Future<void> main() async {
  final session = await bsky.createSession(
    identifier: 'HANDLE_OR_EMAIL',
    password: 'PASSWORD_OR_APP_PASSWORD',
  );

  final bluesky = bsky.Bluesky.fromSession(session.data);

  //! Let's reply to this post.
  final root = await bluesky.feeds.createPost(
    text: 'Reply to me',
  );

  await bluesky.feeds.createPost(
    text: 'Hello, Bluesky!',

    //! Add this.
    reply: bsky.ReplyRef(
      root: root.data,
      parent: root.data,
    ),
  );
}
Enter fullscreen mode Exit fullscreen mode

With the above codes, you can easily reply to a specific post!

And you may have noticed that the ReplyRef object has two arguments, root and parent. With these arguments, you can create a thread, but if you simply want to create a series of threads, the bluesky package provides an easier way.

Let's use createThread method.

import 'package:bluesky/bluesky.dart' as bsky;

Future<void> main() async {
  final session = await bsky.createSession(
    identifier: 'HANDLE_OR_EMAIL',
    password: 'PASSWORD_OR_APP_PASSWORD',
  );

  final bluesky = bsky.Bluesky.fromSession(session.data);

  //! Let's reply to this post.
  final root = await bluesky.feeds.createThread([
    bsky.ThreadParam(text: 'First post'),
    bsky.ThreadParam(text: 'Second post'),
    bsky.ThreadParam(text: 'Third post'),
  ]);

  //! This is the root ref from first post.
  print(root);
}
Enter fullscreen mode Exit fullscreen mode

Using createThread, it is very easy to create a series of threads.

Mentions and Links

In services similar to Bluesky, such as Twitter and Mastodon, mentions and hyperlinks in the text are automatically set up without any special attention. However, this is not the case at Bluesky.

For example, how would the status of this post appear on Bluesky's UI if the previous code were modified as follows?

import 'package:bluesky/bluesky.dart' as bsky;

Future<void> main() async {
  final session = await bsky.createSession(
    identifier: 'HANDLE_OR_EMAIL',
    password: 'PASSWORD_OR_APP_PASSWORD',
  );

  final bluesky = bsky.Bluesky.fromSession(session.data);

  final ref = await bluesky.feeds.createPost(
    text: 'I am @shinyakato.dev! Check https://shinyakato.dev',
  );
}
Enter fullscreen mode Exit fullscreen mode

The answer is, it will post the raw I am @shinyakato.dev! Check https://shinyakato.dev text with no link set in the mentions and hyperlinks.

So, if you are asking if you can't set up links in the Bluesky API, of course you can! You just need to set a special parameter called facet.

Modify the createPost method in the previous example as follows.

await bluesky.feeds.createPost(
  text: 'I am @shinyakato.dev! Check https://shinyakato.dev',

  //! Set Facets.
  facets: [
    //! Facet for mention like "@shinyakato.dev"
    bsky.Facet(
      index: bsky.ByteSlice(
        byteStart: 5,
        byteEnd: 20,
      ),
      features: [
        bsky.FacetFeature.mention(
          data: bsky.FacetMention(
            did: 'did:plc:iijrtk7ocored6zuziwmqq3c',
          ),
        )
      ],
    ),

    //! Facet for link like "https://shinyakato.dev"
    bsky.Facet(
      index: bsky.ByteSlice(
        byteStart: 28,
        byteEnd: 50,
      ),
      features: [
        bsky.FacetFeature.link(
          data: bsky.FacetLink(
            uri: 'https://shinyakato.dev',
          ),
        )
      ],
    ),
  ],
);
Enter fullscreen mode Exit fullscreen mode

Setting up this facet is one of the most difficult operations to enable mentions and hyperlinks using the Bluesky API.

The mechanics are simple, but you need to define the ByteSlice as the start and end position of the specific token you want to link to, such as a mentions or hyperlinks in the text, and even how you want the link to be placed as features.

In other words, this mechanism can be used to create a link that is completely different from the mentions or hyperlinks contained in the text.

However, I feel many people simply want mentions and hyperlinks like those offered by Twitter and Mastodon. Is there an easier solution to this complicated facet? Of course there is! But use the bluesky_text package, which is different from the bluesky package.

Let's check how to use bluesky_text in the next section!

Use bluesky_text for Easy facet Resolution

As mentioned in the previous section, activating links in text using the Bluesky API requires a facet setting, which is a somewhat difficult operation. If you need simple mentions and hyperlinks like Twitter or Mastodon, use bluesky_text.

bluesky_text is another package I am developing, designed to integrate very easily with the bluesky package.

bluesky_text | Dart package

Provides the easiest and most powerful way to analyze the text for Bluesky Social.

favicon pub.dev

And install it like bluesky package.

With Dart:

dart pub add bluesky_text
Enter fullscreen mode Exit fullscreen mode
dart pub get
Enter fullscreen mode Exit fullscreen mode

With Flutter:

flutter pub add bluesky_text
Enter fullscreen mode Exit fullscreen mode
flutter pub get
Enter fullscreen mode Exit fullscreen mode

It is successful if it looks like this in your pubspec.yaml and this article uses bluesky_text v0.2.2.

name: bluesky_post
description: A sample command-line application.
version: 1.0.0
environment:
  sdk: ^3.0.0

dependencies:
  bluesky: ^0.6.0
  bluesky_text: ^0.2.2

dev_dependencies:
  lints: ^2.0.0
  test: ^1.21.0
Enter fullscreen mode Exit fullscreen mode

Okay let's modify the complex facet setup process described earlier as follows.

import 'package:bluesky/bluesky.dart' as bsky;
import 'package:bluesky_text/bluesky_text.dart';

Future<void> main() async {
  final session = await bsky.createSession(
    identifier: 'HANDLE_OR_EMAIL',
    password: 'PASSWORD_OR_APP_PASSWORD',
  );

  final bluesky = bsky.Bluesky.fromSession(session.data);

  //! Just pass text to BlueskyText.
  final text = BlueskyText('I am @shinyakato.dev! Check https://shinyakato.dev');

  //! Extract all the mentions and hyperlinks in the text
  //! and get the facet as JSON.
  final facets = await text.entities.toFacets();

  await bluesky.feeds.createPost(
    //! This is the text you passed to BlueskyText.
    text: text.value,

    //! Convert to Facet objects.
    facets: facets.map(bsky.Facet.fromJson).toList(),
  );
}
Enter fullscreen mode Exit fullscreen mode

Where the heck did the difficult facet process of a few minutes ago disappear to?! This is bluesky_text magic, all you have to do is just pass the text you want to post to the constructor when you create an instance of the BlueskyText object and call .entities.toFacet().

With just this, you can retrieve facet data contained in the text. Then, to post using the bluesky package, convert the JSON retrieved from toFacet() into a Facet object with facets.map(bsky.Facet.fromJson).toList().

Conclusion

This is how to post to Bluesky Social using the bluesky package. I also introduced a simple solution for facet, which is difficult to configure when using the Bluesky API, using the bluesky_text package.

This article mainly touches on the Dart implementation and omits the Flutter implementation, but the bluesky and bluesky_text packages can also be used in Flutter apps. The implementation method is the same as described in this article and will be easily integrated into your Flutter app!

In addition to the bluesky package and bluesky_text I have presented here, I have also developed many AT Protocol related packages for Dart/Flutter in the following monorepo. If you are interested in AT Protocol related packages for Dart/Flutter, please check it out!

GitHub logo myConsciousness / atproto.dart

๐Ÿฆ‹ AT Protocol and Bluesky things for Dart and Flutter.

bluesky

AT Protocol and Bluesky Social Things for Dart/Flutter ๐Ÿฆ‹


GitHub Sponsor GitHub Sponsor melos Reference

Test/Analyzer codecov Issues Pull Requests Stars Contributors Code size Last Commits License Contributor Covenant


Welcome to atproto.dart ๐Ÿฆ‹

This project will maximize your development productivity about AT Protocol and Bluesky things.

Give a โญ on GitHub repository and follow shinyakato.dev on Bluesky!

1. Motivation ๐Ÿ’ช

AT Protocol and Bluesky are awesome.

This wonderful platform needs a standard and highly integrated SDK atproto.dart provides the best development experience in such matters for Dart/Flutter devs.

2. Packages & Tools โš’๏ธ

2.1. Dart Packages

Package pub.dev Docs
at_identifier: core library for the syntax in the AT Protocol standard pub package README
nsid:
โ€ฆ

Suggestions for improvements and pull requests are also very welcome. Or you can contact me on Bluesky :)

Thank you.

Top comments (0)