Action
Publish to Blyg (Studio)
Posted by miguelito4,
Last update
1 day ago
One-tap posting to a blyg on Blygger Studio 0.9+. A permalink on line one makes it a response. Setup: github.com/miguelito4/drafts-blyg
Steps
-
script
// Publish to blyg (Studio) — a Drafts action that posts the current draft to a // blyg running Blygger Studio 0.9 or later, through its documented owner API. // Paste this into a single Script step. Edit CONFIG below. // // Two ways to use it: // - A plain draft becomes a fragment. // - A draft whose FIRST LINE is a blyg permalink (…/f/{id}/ or …/t/{id}/) // becomes a response to that item: a thread that declares stub_of, with the // rest of the draft as your commentary. Studio adds the citation and sends // the Webmention. // ---- config ---- const CONFIG = Object.assign({ blyg: "https://blyg.example.com/", // your blyg's public address, including any mount path mode: "publish", // "publish", or "draft" to save in Studio without publishing maxChars: 1000, // refuse longer drafts (the spec's recommended cap) }, typeof BLYG_CONFIG_OVERRIDE !== "undefined" ? BLYG_CONFIG_OVERRIDE : {}); // ---- end config ---- const DONE_TAG = CONFIG.mode === "draft" ? "blyg-drafted" : "blyg-published"; function fail(msg) { app.displayErrorMessage(msg); context.fail(msg); } /** The blyg address with exactly one trailing slash. */ function blygBase() { return CONFIG.blyg.replace(/\/*$/, "/"); } /** The owner API is host-rooted, even when the blyg is mounted at a path. */ function apiRoot() { const m = /^(https?:\/\/[^\/?#]+)/.exec(CONFIG.blyg); return m ? m[1] + "/api" : null; } function header(resp, name) { const h = resp.headers || {}; for (const k of Object.keys(h)) if (k.toLowerCase() === name) return h[k]; return null; } function sessionFrom(setCookie) { if (!setCookie) return null; const m = /blyg_session=([^;,\s]+)/.exec(String(setCookie)); return m ? "blyg_session=" + m[1] : null; } /** * A first line that is a blyg permalink makes this a response. * Returns { origin, id, body } or null. */ function parseTarget(text) { const lines = text.split("\n"); const first = lines[0].trim(); const m = /^(https?:\/\/\S+?\/)(f|t)\/([^\/\s?#]+)\/?(?:[?#]\S*)?$/.exec(first); if (!m) return null; return { origin: m[1], id: m[3], body: lines.slice(1).join("\n").trim() }; } function api(method, path, body, cookie) { const headers = { "Accept": "application/json" }; if (cookie) headers["Cookie"] = cookie; const req = { url: apiRoot() + path, method, headers }; if (body !== null && body !== undefined) { req.data = body; req.encoding = "json"; } return HTTP.create().request(req); } function apiError(resp, what) { let detail = ""; try { const j = JSON.parse(resp.responseText || "{}"); if (j.error) detail = ": " + j.error; } catch (e) {} switch (resp.statusCode) { case 401: return `${what} was refused (401). The Studio session didn't take; run the action again.`; case 404: return `${what} not found (404). This action needs Blygger Studio 0.9 or later, and CONFIG.blyg must be your blyg's address.`; case 409: return `${what} conflicted with a change made elsewhere (409). Try again.`; default: return `${what} failed (${resp.statusCode})${detail}`; } } /** Sign in at {blyg}/studio/login. Returns { cookie } or { error }. */ function login(password) { const http = HTTP.create(); try { http.followRedirects = false; } catch (e) {} const r = http.request({ url: blygBase() + "studio/login", method: "POST", data: { password }, encoding: "form", }); if (r.statusCode === 403) return { error: "Wrong Studio password. Forget it under Drafts › Settings › Credentials and run again." }; const cookie = sessionFrom(header(r, "set-cookie")); if (cookie) return { cookie }; // The redirect may have been followed, leaving the cookie in the system // cookie store rather than in the headers we can see. Ask the API. const probe = api("GET", "/settings", null, null); if (probe.statusCode === 200) return { cookie: null }; return { error: `Couldn't sign in to Studio (login ${r.statusCode}, check ${probe.statusCode}). Is CONFIG.blyg right?` }; } /** Latest public version of a remote (or local) item. */ function fetchTarget(t) { const r = HTTP.create().request({ url: `${t.origin}items/${t.id}.json`, method: "GET", headers: { "Accept": "application/json" } }); if (!r.success) return { error: `Couldn't read the item you're responding to (${r.statusCode}). Is the first line its permalink?` }; let doc; try { doc = JSON.parse(r.responseText); } catch (e) { return { error: "The item you're responding to isn't a blyg item document." }; } if (doc.kind === "withdrawn") return { error: "That item has been withdrawn; there's nothing to respond to." }; if (!Number.isInteger(doc.version) || doc.version < 1) return { error: "That item document has no usable version." }; return { ref: { origin: doc.origin || t.origin, id: doc.id || t.id, version: doc.version } }; } function main() { if (CONFIG.blyg.includes("example.com") || !apiRoot()) return fail("Set CONFIG.blyg to your blyg's address first."); if (CONFIG.mode !== "publish" && CONFIG.mode !== "draft") return fail('CONFIG.mode must be "publish" or "draft".'); const text = draft.content.trim(); if (!text) return fail("Empty draft."); if (draft.hasTag(DONE_TAG)) return fail("Already sent. Duplicate the draft to send it again."); const target = parseTarget(text); const words = target ? target.body : text; if (!words) return fail("Add your commentary below the link."); if (words.length > CONFIG.maxChars) return fail(`${words.length} chars, over the ${CONFIG.maxChars} cap.`); const cred = Credential.create( `Blygger Studio ${blygBase()}`, "Your Studio owner password (the one you sign in to /studio with)." ); cred.addPasswordField("password", "Password"); if (!cred.authorize()) return fail("No Studio password provided."); let body; if (target) { const t = fetchTarget(target); if (t.error) return fail(t.error); body = { mode: "blank", kind: "thread", content_md: words, stub_of: t.ref }; } else { body = { mode: "blank", kind: "fragment", content_md: words }; } const session = login(cred.getValue("password")); if (session.error) return fail(session.error); const created = api("POST", "/items", body, session.cookie); if (created.statusCode !== 201) { console.log(created.responseText); return fail(apiError(created, "Creating the item")); } const item = JSON.parse(created.responseText); const page = blygBase() + (item.kind === "thread" ? "t/" : "f/") + item.id + "/"; if (CONFIG.mode === "publish") { const pub = api("POST", `/items/${item.id}/publish`, {}, session.cookie); if (!pub.success) { console.log(pub.responseText); return fail(apiError(pub, `Publishing (draft ${item.id} was saved in Studio)`)); } } draft.addTag(DONE_TAG); draft.update(); console.log(page); app.displaySuccessMessage(CONFIG.mode === "publish" ? (target ? "Response published" : "Published") : "Saved to Studio"); } main();
Options
-
After Success Trash Notification Info Log Level Info
Items available in the Drafts Directory are uploaded by community members. Use appropriate caution reviewing downloaded items before use.