├── .dockerignore ├── bin └── server.dart ├── test └── server_test.dart ├── pubspec.yaml ├── .github └── workflows │ └── bluesky-post.yml ├── .gitignore ├── Dockerfile ├── CHANGELOG.md ├── action.yaml ├── LICENSE ├── lib └── post.dart └── README.md /.dockerignore: -------------------------------------------------------------------------------- 1 | .dockerignore 2 | Dockerfile 3 | build/ 4 | .dart_tool/ 5 | .git/ 6 | .github/ 7 | .gitignore 8 | .idea/ 9 | .packages 10 | -------------------------------------------------------------------------------- /bin/server.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Shinya Kato. All rights reserved. 2 | // Redistribution and use in source and binary forms, with or without 3 | // modification, are permitted provided the conditions. 4 | 5 | import 'package:bluesky_post/post.dart'; 6 | 7 | Future main(List arguments) async => await post(); 8 | -------------------------------------------------------------------------------- /test/server_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Shinya Kato. All rights reserved. 2 | // Redistribution and use in source and binary forms, with or without 3 | // modification, are permitted provided the conditions. 4 | 5 | import 'package:test/test.dart'; 6 | 7 | void main() { 8 | test('', () { 9 | // 10 | }); 11 | } 12 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: bluesky_post 2 | description: A server app using the shelf package and Docker. 3 | version: 5.0.0 4 | publish_to: none 5 | environment: 6 | sdk: ^3.0.0 7 | 8 | dependencies: 9 | actions_toolkit_dart: ^0.5.1 10 | bluesky: ^0.10.4 11 | bluesky_text: ^0.6.2 12 | http: ^1.1.0 13 | 14 | dev_dependencies: 15 | lints: ^2.0.1 16 | test: ^1.21.0 17 | import_sorter: ^4.6.0 18 | 19 | import_sorter: 20 | emojis: true 21 | -------------------------------------------------------------------------------- /.github/workflows/bluesky-post.yml: -------------------------------------------------------------------------------- 1 | name: Send Bluesky Post 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | jobs: 7 | post: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: myConsciousness/bluesky-post@v2 11 | with: 12 | text: "Test post from Github Actions powered by 'myConsciousness/bluesky-post'." 13 | identifier: ${{ secrets.BLUESKY_IDENTIFIER }} 14 | password: ${{ secrets.BLUESKY_PASSWORD }} 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://www.dartlang.org/guides/libraries/private-files 2 | 3 | # Files and directories created by pub 4 | .dart_tool/ 5 | .packages 6 | build/ 7 | # If you're building an application, you may want to check-in your pubspec.lock 8 | pubspec.lock 9 | 10 | # Directory created by dartdoc 11 | # If you don't generate documentation locally you can remove this line. 12 | doc/api/ 13 | 14 | # Avoid committing generated Javascript files: 15 | *.dart.js 16 | *.info.json # Produced by the --dump-info flag. 17 | *.js # When generated by dart2js. Don't specify *.js if your 18 | # project includes source files written in JavaScript. 19 | *.js_ 20 | *.js.deps 21 | *.js.map 22 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Specify the Dart SDK base image version using dart: (ex: dart:2.12) 2 | FROM dart:stable AS build 3 | 4 | # Resolve app dependencies. 5 | WORKDIR /app 6 | COPY pubspec.* ./ 7 | RUN dart pub get 8 | 9 | # Copy app source code and AOT compile it. 10 | COPY . . 11 | # Ensure packages are still up-to-date if anything has changed 12 | RUN dart pub get --offline 13 | RUN dart compile exe bin/server.dart -o bin/server 14 | 15 | # Build minimal serving image from AOT-compiled `/server` and required system 16 | # libraries and configuration files stored in `/runtime/` from the build stage. 17 | FROM scratch 18 | COPY --from=build /runtime/ / 19 | COPY --from=build /app/bin/server /app/bin/ 20 | 21 | # Start server. 22 | EXPOSE 8080 23 | CMD ["/app/bin/server"] 24 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Release Note 2 | 3 | ## v4 4 | 5 | - Supported `langs` and `labels` as optional parameters. Pass them as CSV format. ([#668](https://github.com/myConsciousness/atproto.dart/issues/668)) 6 | 7 | ## v3 8 | 9 | - Fixed to automatically extract mentions and links contained in text and set facets appropriately. ([#384](https://github.com/myConsciousness/atproto.dart/issues/384)) 10 | - Support for uploading images from a specified file path. ([#384](https://github.com/myConsciousness/atproto.dart/issues/384)) 11 | 12 | ## v2 13 | 14 | - Allows the user to specify a specific authority to post to. ([#66](https://github.com/myConsciousness/atproto.dart/issues/66)) 15 | - Use `service` parameter to specify authority. The default is `bsky.social`. 16 | - The number of retries can now be specified. ([#126](https://github.com/myConsciousness/atproto.dart/issues/126)) 17 | - Fixed from `handle` to `identifier`. 18 | 19 | ## v1 20 | 21 | - First release! 22 | 23 | ## v0 24 | 25 | - Let's start. 26 | -------------------------------------------------------------------------------- /action.yaml: -------------------------------------------------------------------------------- 1 | name: "Send Bluesky Post" 2 | description: "Provide an easy way to post to Bluesky Social from Github Actions." 3 | author: 'Shinya Kato' 4 | inputs: 5 | text: 6 | description: Text to be posted to Bluesky. 7 | media: 8 | description: File path of the media to be attached to the Post. 9 | media-alt: 10 | description: Alt Text to be assigned to uploaded Media. 11 | link-preview-url: 12 | description: URL for link preview. 13 | langs: 14 | description: A collection of well-formed BCP47 language tags in CSV format. 15 | labels: 16 | description: A collection of self labels in CSV format. 17 | tags: 18 | description: A collection of tags in CSV format. 19 | identifier: 20 | description: > 21 | Handle name or email of the user who logs into Bluesky. 22 | This information is used by the ATP server to establish a session. 23 | For example, "shinyakato.bsky.social". 24 | password: 25 | description: > 26 | Password of the user who logs into Bluesky. 27 | This information is used by the ATP server to establish a session. 28 | service: 29 | description: > 30 | Specify the authority of the ATP server to which you wish to post. 31 | If omitted, it will always be "bsky.social". 32 | retry-count: 33 | description: > 34 | Specify the number of retry on server error or network error. 35 | If omitted, it will always be "5". 36 | 37 | runs: 38 | using: "docker" 39 | image: "Dockerfile" 40 | branding: 41 | icon: edit 42 | color: blue 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2023, Shinya Kato 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | 8 | 1. Redistributions of source code must retain the above copyright notice, this 9 | list of conditions and the following disclaimer. 10 | 11 | 2. Redistributions in binary form must reproduce the above copyright notice, 12 | this list of conditions and the following disclaimer in the documentation 13 | and/or other materials provided with the distribution. 14 | 15 | 3. Neither the name of the copyright holder nor the names of its 16 | contributors may be used to endorse or promote products derived from 17 | this software without specific prior written permission. 18 | 19 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 20 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 21 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 22 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 23 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 24 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 25 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 26 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 27 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | -------------------------------------------------------------------------------- /lib/post.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Shinya Kato. All rights reserved. 2 | // Redistribution and use in source and binary forms, with or without 3 | // modification, are permitted provided the conditions. 4 | 5 | // 🎯 Dart imports: 6 | import 'dart:async'; 7 | import 'dart:io'; 8 | 9 | // 📦 Package imports: 10 | import 'package:actions_toolkit_dart/core.dart' as core; 11 | import 'package:bluesky/bluesky.dart' as bsky; 12 | import 'package:bluesky/cardyb.dart' as cardyb; 13 | import 'package:bluesky_text/bluesky_text.dart'; 14 | import 'package:http/http.dart' as http; 15 | 16 | const _linkConfig = LinkConfig( 17 | excludeProtocol: true, 18 | enableShortening: true, 19 | ); 20 | 21 | Future post() async { 22 | final bluesky = await _bluesky; 23 | 24 | final text = BlueskyText( 25 | core.getInput( 26 | name: 'text', 27 | options: core.InputOptions(required: true), 28 | ), 29 | linkConfig: _linkConfig, 30 | ).format(); 31 | 32 | final facets = await text.entities.toFacets(service: _service); 33 | 34 | final createdPost = await bluesky.feeds.createPost( 35 | text: text.value, 36 | facets: facets.map(bsky.Facet.fromJson).toList(), 37 | embed: await _getEmbed(bluesky), 38 | languageTags: _langs, 39 | labels: _labels, 40 | tags: _tags, 41 | ); 42 | 43 | core.info(message: 'Sent a post successfully!'); 44 | core.info(message: 'cid = [${createdPost.data.cid}]'); 45 | core.info(message: 'uri = [${createdPost.data.uri}]'); 46 | } 47 | 48 | Future _getEmbed(final bsky.Bluesky bluesky) async { 49 | try { 50 | final uploadedMedia = await _uploadMedia(bluesky); 51 | if (uploadedMedia != null) { 52 | return bsky.Embed.images( 53 | data: bsky.EmbedImages( 54 | images: [ 55 | bsky.Image( 56 | alt: core.getInput( 57 | name: 'media-alt', 58 | options: core.InputOptions(trimWhitespace: true), 59 | ), 60 | image: uploadedMedia.blob, 61 | ) 62 | ], 63 | ), 64 | ); 65 | } 66 | 67 | final preview = await cardyb.findLinkPreview(Uri.parse(core.getInput( 68 | name: 'link-preview-url', 69 | options: core.InputOptions(trimWhitespace: true), 70 | ))); 71 | 72 | final uploadedPreview = await _uploadLinkPreview( 73 | bluesky, 74 | preview.data.image, 75 | ); 76 | 77 | return bsky.Embed.external( 78 | data: bsky.EmbedExternal( 79 | external: bsky.EmbedExternalThumbnail( 80 | uri: preview.data.url, 81 | title: preview.data.title, 82 | description: preview.data.description, 83 | blob: uploadedPreview?.blob, 84 | ), 85 | ), 86 | ); 87 | } catch (_) { 88 | return null; 89 | } 90 | } 91 | 92 | Future _getSession( 93 | final String service, 94 | final bsky.RetryConfig retryConfig, 95 | ) async { 96 | final session = await bsky.createSession( 97 | service: service, 98 | identifier: core.getInput( 99 | name: 'identifier', 100 | options: core.InputOptions( 101 | required: true, 102 | trimWhitespace: true, 103 | ), 104 | ), 105 | password: core.getInput( 106 | name: 'password', 107 | options: core.InputOptions( 108 | required: true, 109 | trimWhitespace: true, 110 | ), 111 | ), 112 | retryConfig: retryConfig, 113 | ); 114 | 115 | return session.data; 116 | } 117 | 118 | Future get _bluesky async { 119 | final service = _service; 120 | final retryCount = core.getInput(name: 'retry-count'); 121 | 122 | final retryConfig = bsky.RetryConfig( 123 | maxAttempts: retryCount.isEmpty ? 5 : int.parse(retryCount), 124 | ); 125 | 126 | return bsky.Bluesky.fromSession( 127 | await _getSession(service, retryConfig), 128 | service: service, 129 | retryConfig: retryConfig, 130 | ); 131 | } 132 | 133 | String get _service { 134 | final service = core.getInput( 135 | name: 'service', 136 | options: core.InputOptions(trimWhitespace: true), 137 | ); 138 | 139 | return service.isEmpty ? 'bsky.social' : service; 140 | } 141 | 142 | Future _uploadMedia(final bsky.Bluesky bluesky) async { 143 | final mediaPath = core.getInput( 144 | name: 'media', 145 | options: core.InputOptions(trimWhitespace: true), 146 | ); 147 | 148 | if (mediaPath.isEmpty) { 149 | return null; 150 | } 151 | 152 | final uploaded = await bluesky.repositories.uploadBlob( 153 | File(mediaPath).readAsBytesSync(), 154 | ); 155 | 156 | return uploaded.data; 157 | } 158 | 159 | Future _uploadLinkPreview( 160 | final bsky.Bluesky bluesky, 161 | final String previewImage, 162 | ) async { 163 | if (previewImage.isEmpty) { 164 | return null; 165 | } 166 | 167 | final image = await http.get(Uri.parse(previewImage)); 168 | if (image.statusCode != 200) return null; 169 | 170 | final uploaded = await bluesky.repositories.uploadBlob(image.bodyBytes); 171 | 172 | return uploaded.data; 173 | } 174 | 175 | List? get _langs { 176 | final langs = core.getInput( 177 | name: 'langs', 178 | options: core.InputOptions(trimWhitespace: true), 179 | ); 180 | 181 | if (langs.isEmpty) { 182 | return null; 183 | } 184 | 185 | return langs.split(','); 186 | } 187 | 188 | bsky.Labels? get _labels { 189 | final labels = core.getInput( 190 | name: 'labels', 191 | options: core.InputOptions(trimWhitespace: true), 192 | ); 193 | 194 | if (labels.isEmpty) { 195 | return null; 196 | } 197 | 198 | return bsky.Labels.selfLabels( 199 | data: bsky.SelfLabels( 200 | values: labels.split(',').map((e) => bsky.SelfLabel(value: e)).toList(), 201 | ), 202 | ); 203 | } 204 | 205 | List? get _tags { 206 | final tags = core.getInput( 207 | name: 'tags', 208 | options: core.InputOptions(trimWhitespace: true), 209 | ); 210 | 211 | if (tags.isEmpty) { 212 | return null; 213 | } 214 | 215 | return tags.split(','); 216 | } 217 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![GitHub Sponsor](https://img.shields.io/static/v1?label=Sponsor&message=%E2%9D%A4&logo=GitHub&color=ff69b4)](https://github.com/sponsors/myConsciousness) 2 | [![GitHub Sponsor](https://img.shields.io/static/v1?label=Maintainer&message=myConsciousness&logo=GitHub&color=00acee)](https://github.com/myConsciousness) 3 | 4 | [![Powered by atproto](https://img.shields.io/badge/Powered%20by-atproto-00acee.svg)](https://github.com/myConsciousness/atproto.dart/tree/main/packages/atproto) 5 | [![Powered by bluesky](https://img.shields.io/badge/Powered%20by-bluesky-00acee.svg)](https://github.com/myConsciousness/atproto.dart/tree/main/packages/bluesky) 6 | [![Powered by bluesky_text](https://img.shields.io/badge/Powered%20by-bluesky_text-00acee.svg)](https://github.com/myConsciousness/atproto.dart/tree/main/packages/bluesky_text) 7 | 8 | # Send a Post to Bluesky from GitHub Actions Workflow 9 | 10 | Use this action to send a post from GitHub actions workflow. 11 | 12 | This action is implemented in the Dart language and uses **[bluesky](https://github.com/myConsciousness/atproto.dart/tree/main/packages/bluesky)** for posting to Bluesky Social. 13 | 14 | ## Workflow Usage 15 | 16 | Configure your workflow to use `myConsciousness/bluesky-post@v5`, 17 | and provide the post you want to send as the `text` input. 18 | 19 | Provide Bluesky's ATP server with `identifier` (handle or email) and `password` to create a session. 20 | 21 | For example: 22 | 23 | ```yml 24 | name: Send Bluesky Post 25 | 26 | on: 27 | [push] 28 | 29 | jobs: 30 | post: 31 | runs-on: ubuntu-latest 32 | steps: 33 | - uses: myConsciousness/bluesky-post@v5 34 | with: 35 | text: "Hello, Bluesky!" 36 | identifier: ${{ secrets.BLUESKY_IDENTIFIER }} 37 | password: ${{ secrets.BLUESKY_PASSWORD }} 38 | ``` 39 | 40 | Now whenever you push something to your repository, GitHub Actions 41 | will post to Bluesky on your behalf. 42 | 43 | > **Note**
44 | > In the Bluesky API, the `mention`, `link` and `tag` functions will not work unless 45 | > the `facet` parameter is set correctly when the request is sent, 46 | > but this Action will automatically extract valid handle and link 47 | > from the text and set the facet. 48 | 49 | ## Specify Authority 50 | 51 | Bluesky Social is a distributed microservice. 52 | So you may possibly want to post to a ATP server other than `bsky.social`. 53 | 54 | In that case, set the `service` parameter to the authority you wish to post as follows. 55 | If the `service` parameter is omitted, the default is `bsky.social`. 56 | 57 | ```yml 58 | name: Send Bluesky Post 59 | 60 | on: 61 | [push] 62 | 63 | jobs: 64 | post: 65 | runs-on: ubuntu-latest 66 | steps: 67 | - uses: myConsciousness/bluesky-post@v5 68 | with: 69 | text: "Hello, Bluesky!" 70 | identifier: ${{ secrets.BLUESKY_IDENTIFIER }} 71 | password: ${{ secrets.BLUESKY_PASSWORD }} 72 | service: "boobee.blue" 73 | ``` 74 | 75 | ## Retry 76 | 77 | Server error or network errors may temporarily occur during API communication to the ATP server. 78 | In such cases, retrying at regular intervals may result in successful processing. 79 | 80 | This Actions supports `Retry`, and you can specify the maximum number of retries. 81 | The default retry count is 5. 82 | 83 | You can specify the following. 84 | 85 | ```yml 86 | name: Send Bluesky Post 87 | 88 | on: 89 | [push] 90 | 91 | jobs: 92 | post: 93 | runs-on: ubuntu-latest 94 | steps: 95 | - uses: myConsciousness/bluesky-post@v5 96 | with: 97 | text: "Hello, Bluesky!" 98 | identifier: ${{ secrets.BLUESKY_IDENTIFIER }} 99 | password: ${{ secrets.BLUESKY_PASSWORD }} 100 | retry-count: 5 101 | ``` 102 | 103 | ## Attach Media 104 | 105 | You can also post a text with an image of a specified file path attached. 106 | 107 | ```yml 108 | name: Send Bluesky Post 109 | 110 | on: 111 | [push] 112 | 113 | jobs: 114 | post: 115 | runs-on: ubuntu-latest 116 | steps: 117 | # You must checkout resources 118 | - uses: actions/checkout@v3 119 | 120 | - uses: myConsciousness/bluesky-post@v5 121 | with: 122 | text: "Hello, Bluesky!" 123 | media: cool_photo.png 124 | media-alt: "This is a cool photo!" 125 | identifier: ${{ secrets.BLUESKY_IDENTIFIER }} 126 | password: ${{ secrets.BLUESKY_PASSWORD }} 127 | ``` 128 | 129 | ## Attach Language Tags 130 | 131 | You can give the post you are sending **BCP47 format language tags**. 132 | One or more language tags can be set and passed to the `langs` parameter in **_CSV format_** as follows. 133 | 134 | ```yml 135 | name: Send Bluesky Post 136 | 137 | on: 138 | [push] 139 | 140 | jobs: 141 | post: 142 | runs-on: ubuntu-latest 143 | steps: 144 | - uses: myConsciousness/bluesky-post@v5 145 | with: 146 | text: "Hello, Bluesky!" 147 | langs: "en,ja" 148 | identifier: ${{ secrets.BLUESKY_IDENTIFIER }} 149 | password: ${{ secrets.BLUESKY_PASSWORD }} 150 | ``` 151 | 152 | ## Attach Self Labels 153 | 154 | You can **label** any post you send. 155 | You can set one or more labels, and pass the value of any label in the `labels` parameter in **_CSV format_** as follows. 156 | 157 | ```yml 158 | name: Send Bluesky Post 159 | 160 | on: 161 | [push] 162 | 163 | jobs: 164 | post: 165 | runs-on: ubuntu-latest 166 | steps: 167 | - uses: myConsciousness/bluesky-post@v5 168 | with: 169 | text: "Hello, Bluesky!" 170 | labels: "spam,porn" 171 | identifier: ${{ secrets.BLUESKY_IDENTIFIER }} 172 | password: ${{ secrets.BLUESKY_PASSWORD }} 173 | ``` 174 | 175 | ## Attach Tags 176 | 177 | You can **tag** any post you send. 178 | You can set one or more tags, and pass the value of any tag in the `tags` parameter in **_CSV format_** as follows. 179 | 180 | ```yml 181 | name: Send Bluesky Post 182 | 183 | on: 184 | [push] 185 | 186 | jobs: 187 | post: 188 | runs-on: ubuntu-latest 189 | steps: 190 | - uses: myConsciousness/bluesky-post@v5 191 | with: 192 | text: "Hello, Bluesky!" 193 | tags: "bluesky,awesome" 194 | identifier: ${{ secrets.BLUESKY_IDENTIFIER }} 195 | password: ${{ secrets.BLUESKY_PASSWORD }} 196 | ``` 197 | 198 | > **Note**
199 | > The value specified in the `tags` parameter is different from hashtags in the text, 200 | > which are generally displayed as metadata about the post in Bluesky's clients. 201 | 202 | ## Markdown Link 203 | 204 | You can specify links in a generic markdown format. 205 | 206 | ```yml 207 | name: Send Bluesky Post 208 | 209 | on: 210 | [push] 211 | 212 | jobs: 213 | post: 214 | runs-on: ubuntu-latest 215 | steps: 216 | - uses: myConsciousness/bluesky-post@v5 217 | with: 218 | text: "[This is a markdown link!](https://atprotodart.com)" 219 | identifier: ${{ secrets.BLUESKY_IDENTIFIER }} 220 | password: ${{ secrets.BLUESKY_PASSWORD }} 221 | ``` 222 | 223 | ## Attach Link Preview 224 | 225 | You can attach link preview (link card) with `link-preview-url` parameter. 226 | 227 | If a `link-preview-url` parameter is also specified while a `media` parameter is present, 228 | the `media` parameter will be used first. 229 | 230 | ```yml 231 | name: Send Bluesky Post 232 | 233 | on: 234 | [push] 235 | 236 | jobs: 237 | post: 238 | runs-on: ubuntu-latest 239 | steps: 240 | - uses: myConsciousness/bluesky-post@v5 241 | with: 242 | text: "Hello, Bluesky!" 243 | link-preview-url: "https://atprotodart.com" 244 | identifier: ${{ secrets.BLUESKY_IDENTIFIER }} 245 | password: ${{ secrets.BLUESKY_PASSWORD }} 246 | ``` 247 | 248 | ## More Information 249 | 250 | **bluesky_post** was designed and implemented by **_Shinya Kato ([@myConsciousness](https://github.com/myConsciousness))_**. 251 | 252 | - [Creator Profile](https://github.com/myConsciousness) 253 | - [License](https://github.com/myConsciousness/atproto.dart/blob/main/LICENSE) 254 | - [Release Note](https://github.com/myConsciousness/atproto.dart/releases) 255 | - [Bug Report](https://github.com/myConsciousness/atproto.dart/issues) 256 | --------------------------------------------------------------------------------