Action
Post to Bluesky
UPDATES
8 days ago
more fixes for link parsing
8 days ago
more fixes for link parsing
9 days ago
add facets for links and hashtags that properly create tapable links / hashtags now
21 days ago
Fix logging of response data
29 days ago
remove assigned tags
29 days ago
changed login details to use drafts credentials
about 1 month ago
Custom domain support (hopefully)
created by @FlohGro / more on my Blog
Post to Bluesky
Post the content of the current draft to your Bluesky account
Attention: this action only works for bsky.social
[Configuration]
The first time you use this action you need to configure your username and an app password. They will be stored in Drafts internal credential store.
Be prepared to type your username on Bluesky (e.g. flohgro.bsky.social
) without the @
sign and to insert your app password that you can create as follow:
- open your Bluesky settings (on web or the iOS app)
- select Privacy and security
- select App passwords
- Tap Add App Password
- choose a name for the password (e.g. Drafts)
- Tap Next
- Copy the displayed password
- Tap Done
- Insert the created password into the configuration of the action as described above
If you changed your user handle to your own domain name use that domain as username (e.g. flohgro.com).
[Usage]
After configuring it properly you can post the content of the current draft with this action. The action will not check for invalid length of the post so if something fails, you can check the action log for the error message (e.g. if your text was too long).
If you ever need to change the login details you can forget the credential as documented here.
If you find this useful and want to support me you can donate or buy me a coffee
Steps
-
script
const content = draft.content let bCredential; function configureUser() { bCredential = Credential.createWithUsernamePassword("BlueskyAppPassword", "Bluesky credentials, add your username (e.g. flohgro.bsky.social) and your app password that you created before.") let result = bCredential.authorize() if(!result){ console.log("setting/retrieving credential failed.") app.displayErrorMessage("setting/retrieving credential failed.") } return result } function authenticateAndPost() { let username = bCredential.getValue("username") let appPassword = bCredential.getValue("password") // Base URL for the Bluesky API const baseUrl = "https://bsky.social/xrpc/"; // HTTP object for making requests const http = HTTP.create(); // Step 1: Get the Bearer token (Session Token) const authRequest = { url: `${baseUrl}com.atproto.server.createSession`, method: "POST", headers: { "Content-Type": "application/json", }, data: { identifier: username, password: appPassword, }, }; const authResponse = http.request(authRequest); if (authResponse.statusCode === 200) { const session = JSON.parse(authResponse.responseText); const bearerToken = session.accessJwt; const facets = createFacets(content); // Step 2: Create a post using the Bearer token const postContent = { repo: username, collection: "app.bsky.feed.post", record: { text: content, facets: facets, createdAt: new Date().toISOString(), // Current timestamp }, }; const postRequest = { url: `${baseUrl}com.atproto.repo.createRecord`, method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${bearerToken}`, }, data: postContent, }; const postResponse = http.request(postRequest); if (postResponse.statusCode === 200) { const data = postResponse.responseText; console.log("Post created successfully:\n" + data); return true } else { app.displayErrorMessage("posting failed"); const message = "Error: " + postResponse.statusCode + "\n\n" + postResponse.responseText; context.fail(message); return false } } else { app.displayErrorMessage("authentication failed") const message = "Error: " + authResponse.statusCode + "\n\n" + authResponse.responseText; context.fail(message); return false } } // actually run something - configure user and, exectue authenticate and post if (configureUser()) { if(!authenticateAndPost()){ context.fail() } } else { context.fail() } function extractLinks(text) { // const urlRegex = /https?:\/\/[^\s]+/g; const urlRegex = /((?:[a-z][\w-]+:(?:\/{1,3}|[a-z0-9%])|www\d{0,3}[.]|[a-z0-9.\-]+[.][a-z]{2,4}\/)(?:[^\s()<>]+|\(([^\s()<>]+|(\([^\s()<>]+\)))*\))+(?:\(([^\s()<>]+|(\([^\s()<>]+\)))*\)|[^\s`!()\[\]{};:'".,<>?«»""'']))/g const matches = []; let match; while ((match = urlRegex.exec(text)) !== null) { const byteStart = computeByteOffset(text, match.index); const byteEnd = computeByteOffset(text, match.index + match[0].length); matches.push({ byteStart, byteEnd, url: match[0], }); } return matches; } function extractHashtags(text) { const hashtagRegex = /#[\w]+/g; // Matches hashtags like #example const matches = []; let match; while ((match = hashtagRegex.exec(text)) !== null) { const byteStart = computeByteOffset(text, match.index); const byteEnd = computeByteOffset(text, match.index + match[0].length); matches.push({ //index: match.index, byteStart, byteEnd, hashtag: match[0], uri: `https://bsky.app/search?q=%23${encodeURIComponent(match[0].substring(1))}`, }); } return matches; } function computeByteOffset(text, charIndex) { let byteLength = 0; for (let i = 0; i < charIndex; i++) { const codePoint = text.codePointAt(i); // Get the Unicode code point if (codePoint <= 0x007F) { // 1-byte sequence (ASCII) byteLength += 1; } else if (codePoint <= 0x07FF) { // 2-byte sequence byteLength += 2; } else if (codePoint <= 0xFFFF) { // 3-byte sequence byteLength += 3; } else { // 4-byte sequence (e.g., emojis, astral symbols) byteLength += 4; i++; // Skip the next index, as it’s part of the same surrogate pair } } return byteLength; } function createFacets(text) { const links = extractLinks(text).map(link => ({ index: { byteStart: link.byteStart, byteEnd: link.byteEnd, }, features: [ { $type: "app.bsky.richtext.facet#link", uri: link.url, }, ], })); const hashtags = extractHashtags(text).map(tag => ({ index: { byteStart: tag.byteStart, byteEnd: tag.byteEnd, }, features: [ { $type: "app.bsky.richtext.facet#tag", tag: tag.hashtag.replace(/^#/, ''), }, ], })); return [...links, ...hashtags]; }
Options
-
After Success Default , Tags: bluesky, published Notification Info Log Level Info