Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 3 additions & 36 deletions components/bluesky/actions/create-post/create-post.mjs
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import app from "../../bluesky.app.mjs";
import constants from "../../common/constants.mjs";
import textEncoding from "../../common/textEncoding.mjs";

export default {
key: "bluesky-create-post",
name: "Create Post",
description: "Creates a new post on Bluesky. [See the documentation](https://docs.bsky.app/docs/api/com-atproto-repo-create-record).",
version: "0.0.2",
version: "0.1.0",
type: "action",
props: {
app,
Expand All @@ -15,40 +16,6 @@ export default {
description: "The text content of the post.",
},
},
methods: {
parseUrls(text) {
const spans = [];
const urlRegex = /(?:[$|\W])(https?:\/\/(www\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_+.~#?&//=]*[-a-zA-Z0-9@%_+~#//=])?)/g;

let match;
while ((match = urlRegex.exec(text)) !== null) {
spans.push({
start: match.index + 1,
end: urlRegex.lastIndex,
url: match[1],
});
}
return spans;
},
parseFacets(text) {
const facets = [];
for (const link of this.parseUrls(text)) {
facets.push({
index: {
byteStart: link["start"],
byteEnd: link["end"],
},
features: [
{
["$type"]: "app.bsky.richtext.facet#link",
uri: link["url"],
},
],
});
}
return facets;
},
},
async run({ $ }) {
const {
app,
Expand All @@ -62,7 +29,7 @@ export default {
record: {
["$type"]: constants.RESOURCE_TYPE.POST,
text,
facets: this.parseFacets(text),
facets: await textEncoding.detectFacets(text, app),
createdAt: new Date().toISOString(),
},
},
Expand Down
2 changes: 1 addition & 1 deletion components/bluesky/actions/like-post/like-post.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ export default {
key: "bluesky-like-post",
name: "Like Post",
description: "Like a specific post on Bluesky. [See the documentation](https://docs.bsky.app/docs/api/com-atproto-repo-create-record).",
version: "0.0.1",
version: "0.0.2",
type: "action",
props: {
app,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ export default {
key: "bluesky-retrieve-thread",
name: "Retrieve Thread",
description: "Retrieve a full thread of posts. [See the documentation](https://docs.bsky.app/docs/api/app-bsky-feed-get-post-thread).",
version: "0.0.1",
version: "0.0.2",
type: "action",
props: {
app,
Expand Down
127 changes: 127 additions & 0 deletions components/bluesky/common/textEncoding.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import TLDs from "./tlds.mjs";

function isValidDomain(str) {
return !!TLDs.find((tld) => {
const i = str.lastIndexOf(tld);
if (i === -1) {
return false;
}
return str.charAt(i - 1) === "." && i === str.length - tld.length;
});
}

function utf16IndexToUtf8Index(i, utf16) {
const encoder = new TextEncoder();
return encoder.encode(utf16.slice(0, i)).byteLength;
}

async function detectFacets(text, app) {
let match;
const facets = [];

// mentions
const re1 = /(^|\s|\()(@)([a-zA-Z0-9.-]+)(\b)/g;
while ((match = re1.exec(text))) {
if (!isValidDomain(match[3]) && !match[3].endsWith(".test")) {
continue; // probably not a handle
}

const start = text.indexOf(match[3], match.index) - 1;
const handle = await app.resolveHandle({
params: {
handle: match[3],
},
});
facets.push({
$type: "app.bsky.richtext.facet",
index: {
byteStart: utf16IndexToUtf8Index(start, text),
byteEnd: utf16IndexToUtf8Index(start + match[3].length + 1, text),
},
features: [
{
$type: "app.bsky.richtext.facet#mention",
did: handle.did, // must be resolved afterwards
},
],
});
}

// links
const re2 =
/(^|\s|\()((https?:\/\/[\S]+)|((?<domain>[a-z][a-z0-9]*(\.[a-z0-9]+)+)[\S]*))/gim;
while ((match = re2.exec(text))) {
let uri = match[2];
if (!uri.startsWith("http")) {
const domain = match.groups?.domain;
if (!domain || !isValidDomain(domain)) {
continue;
}
uri = `https://${uri}`;
}
const start = text.indexOf(match[2], match.index);
const index = {
start,
end: start + match[2].length,
};
// strip ending puncuation
if (/[.,;!?]$/.test(uri)) {
uri = uri.slice(0, -1);
index.end--;
}
if (/[)]$/.test(uri) && !uri.includes("(")) {
uri = uri.slice(0, -1);
index.end--;
}
facets.push({
index: {
byteStart: utf16IndexToUtf8Index(index.start, text),
byteEnd: utf16IndexToUtf8Index(index.end, text),
},
features: [
{
$type: "app.bsky.richtext.facet#link",
uri,
},
],
});
}

const re3 = /(?:^|\s)(#[^\d\s]\S*)(?=\s)?/g;
while ((match = re3.exec(text))) {
let [
tag,
] = match;
const hasLeadingSpace = /^\s/.test(tag);

tag = tag.trim().replace(/\p{P}+$/gu, ""); // strip ending punctuation

// inclusive of #, max of 64 chars
if (tag.length > 66) continue;

const index = match.index + (hasLeadingSpace
? 1
: 0);

facets.push({
index: {
byteStart: utf16IndexToUtf8Index(index, text),
byteEnd: utf16IndexToUtf8Index(index + tag.length, text), // inclusive of last char
},
features: [
{
$type: "app.bsky.richtext.facet#tag",
tag: tag.replace(/^#/, ""),
},
],
});
}

return facets.length > 0
? facets
: undefined;
}

export default {
detectFacets,
};
Loading
Loading