Action

Post to Bluesky

Posted by FlohGro, Last update 8 days ago

UPDATES

8 days ago

more fixes for link parsing

show all updates...

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
Items available in the Drafts Directory are uploaded by community members. Use appropriate caution reviewing downloaded items before use.