Action
Post Thread
Post a thread to Mastodon. To use, write the full content of the thread in a single draft, with ===
(three equal signs) on lines where you want the posts to be divided.
This action will:
- Split the text of the current draft into separate posts, using the “===“ as a divider.
- Display a preview of the thread as it will be posted. Any errors will be displayed - e.g. if any of the blocks are too long to be a single post.
- If you continue from the preview, the thread will be posted with the first post as a public post, and subsequent posts a unlisted replies.
IMPORTANT: Before using this action, edit the first to “Define Template Tag” steps to enter the Mastodon instance host and credential identifier to use.
For more examples, see our Mastodon Integration Guide
Steps
-
defineTemplateTag
name mastodon-host
template your.mastodon
-
defineTemplateTag
name credential-id
template @agiletortoise
-
script
// max characters per post. Leave room for the thread marker const maxCharacters = 490 // separator to find where to break up the text for the thread const separator = "===" const thread = "🧵" // grab host-id from tags let host = draft.processTemplate("[[mastodon-host]]") let credentialID = draft.processTemplate("[[credential-id]]") // validate values if (host.length == 0 || host == "your.mastodon") { alert(`Mastodon host must be configured to run this action. Edit action and set your Mastodon host name (like "mastodon.social") as the template value in the first "Define Template Tag" action step. `) context.cancel() }
-
script
// Utility functions function dividePosts(input) { var posts = input.split(separator + "\n"); return posts.map(x => x.trim()); } function countString(current, total) { return ` (${current}/${total}) ${thread}`; } function isPostValid(post) { return post.length <= maxCharacters && post.length > 0; } function areAllPostsValid(posts) { if (posts.length == 0) { return false; } for (var post of posts) { if (!isPostValid(post)) { return false; } } return true; } function htmlSafe(s) { return s.replace(/</g, "<").replace(/\n/g, "<br>\n"); } // build HTML preview display the resulting tweets. var html = ["<html><head>"]; html.push("<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">"); html.push("<style>"); html.push("body { background: #666; color: #444; font-family: system-ui, -apple-system; margin: 1em auto; max-width:360px; }"); html.push("p {padding: 1em; background-color: #eee; }"); html.push("p.error{ background-color: #FFCDDD; color: maroon; font-weight: bold; padding: 1.5em; }\n"); html.push("p.invalid{ color: maroon; }\n"); html.push("p.reply { border-left: 5px solid #bbb; }"); html.push("p.preview{ font-style:italic; background-color: #444; color: #aaa; font-size: .9em; }\n"); html.push("span.note{ background-color: maroon; color: white; font-weight: bold; border-radius:3px; padding: .25em; font-size: .8em; }\n"); html.push("</style>"); html.push("</head><body>"); const posts = dividePosts(draft.content); var isValid = areAllPostsValid(posts); if (!isValid) { html.push("<p class='error'>"); html.push(`This draft cannot be posted as a post storm. Be sure to divide the draft into blocks of ${maxCharacters} characters or shorter, with each post separated by a line containing the text “${separator}”`); html.push("</p>"); } else { html.push("<p class='preview'>"); html.push("Preview posts as they will be posted below. Tap continue to post, cancel to go back without posting."); html.push("</p>"); } if (posts.length > 0) { var ct = 1; for (var post of posts) { if (isPostValid(post)) { html.push("<p class='valid" + (ct > 1 ? " reply" : "") + "'>"); html.push(htmlSafe(post)); html.push(countString(ct, posts.length)); html.push("</p>"); } else { html.push("<p class='invalid" + (ct > 1 ? " reply" : "") + "'>"); html.push("<span class='note'>" + post.length + " characters</span> "); html.push(htmlSafe(post)); html.push(countString(ct, posts.length)); html.push("</p>"); } ct++; } } else { html.push("<p>Nothing to post</p>"); } html.push("</body></html>"); draft.setTemplateTag("html", html.join("\n"));
-
htmlpreview
[[html]]
-
script
// If we reach this script // We have a valid set of posts ready // in the `posts` const, we just need to post them... // sleep method to use to avoid rate limiting const sleep = ms => new Promise(resolve => setTimeout(resolve, ms)) if (isValid) { // create Mastodon instance let m = Mastodon.create(host, credentialID) const path = "/api/v1/statuses" let vis = "public" // will change to unlisted for replies let ct = 1 let inReplyTo = "" let hasError = false for (let post of posts) { const content = post + countString(ct, posts.length); let data = { "status": content, "visibility": vis } // if this is the first post // set the reply information to thread post storm if (ct != 1) { data["in_reply_to_id"] = inReplyTo } // make API request let response = m.request({ "path": path, "method": "POST", "data": data }) if (!response.success) { console.log(`Post Failed: ${response.statusCode}, ${response.error}`) hasError = true break } else { inReplyTo = response.responseData["id"] console.log(`Posted to Mastodon: ${response.responseData["url"]}`) } ct++ vis = "unlisted" sleep(500) // avoid rate limiting with little pause } if (!hasError) { console.log("Post storm posted"); } else { console.log("Post storm failed"); context.fail(); } } else { context.cancel(); }
Options
-
After Success Default Notification Info Log Level Info
Items available in the Drafts Directory are uploaded by community members. Use appropriate caution reviewing downloaded items before use.