Skip to content

Commit 1e4003f

Browse files
Merge pull request #59 from dherbst/hashtag-links
Fix bluesky post hashtags not appearing as clickable links.
2 parents d96fbb8 + 8ce110c commit 1e4003f

2 files changed

Lines changed: 63 additions & 7 deletions

File tree

social_posters/bluesky.py

Lines changed: 34 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -68,12 +68,16 @@ def publish(self, post: Post) -> PostResult:
6868
except Exception as exc:
6969
return PostResult(success=False, error_message=str(exc))
7070

71+
text, facets = self._build_text_and_facets(post)
7172
record = {
7273
"$type": "app.bsky.feed.post",
73-
"text": self._format_text(post),
74+
"text": text,
7475
"createdAt": datetime.utcnow().isoformat() + "Z",
7576
}
7677

78+
if facets:
79+
record["facets"] = facets
80+
7781
if image_blob:
7882
record["embed"] = {
7983
"$type": "app.bsky.embed.images",
@@ -143,10 +147,33 @@ def format_post(self, pet):
143147
alt_text=f"Photo of {name}, a {pet.breed} available for adoption",
144148
tags=tags,
145149
)
146-
def _format_text(self, post: Post) -> str:
147-
text = post.text
148-
if post.tags:
149-
tags = " ".join(f"#{tag}" for tag in post.tags if tag)
150-
text = f"{text}\n\n{tags}"
151-
return text[:300]
150+
def _build_text_and_facets(self, post: Post) -> tuple[str, list]:
151+
body = post.text
152+
facets = []
153+
154+
if not post.tags:
155+
return body[:300], facets
156+
157+
tag_strings = [f"#{tag}" for tag in post.tags if tag]
158+
tags_section = " ".join(tag_strings)
159+
separator = "\n\n"
160+
161+
# Truncate body so the full text (body + separator + tags) fits in 300 chars.
162+
max_body = 300 - len(separator) - len(tags_section)
163+
full_text = f"{body[:max_body]}{separator}{tags_section}"
164+
165+
# Compute byte offsets (AT Protocol facets use UTF-8 byte positions).
166+
encoded = full_text.encode("utf-8")
167+
search_from = 0
168+
for tag_str in tag_strings:
169+
tag_bytes = tag_str.encode("utf-8")
170+
idx = encoded.find(tag_bytes, search_from)
171+
if idx != -1:
172+
facets.append({
173+
"index": {"byteStart": idx, "byteEnd": idx + len(tag_bytes)},
174+
"features": [{"$type": "app.bsky.richtext.facet#tag", "tag": tag_str[1:]}],
175+
})
176+
search_from = idx + len(tag_bytes)
177+
178+
return full_text, facets
152179

tests/test_bluesky.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
from abstractions import Post
2+
from social_posters.bluesky import PosterBluesky
3+
4+
5+
class TestBuildTextAndFacets:
6+
def setup_method(self):
7+
self.poster = PosterBluesky.__new__(PosterBluesky)
8+
9+
def test_no_tags_produces_no_facets(self):
10+
post = Post(text="Hello, world!")
11+
text, facets = self.poster._build_text_and_facets(post)
12+
assert text == "Hello, world!"
13+
assert facets == []
14+
15+
def test_tags_produce_facets_with_correct_byte_offsets(self):
16+
post = Post(text="Adopt me!", tags=["AdoptDontShop", "Boston", "DogsOfBluesky"])
17+
text, facets = self.poster._build_text_and_facets(post)
18+
19+
assert text == "Adopt me!\n\n#AdoptDontShop #Boston #DogsOfBluesky"
20+
assert len(facets) == 3
21+
22+
encoded = text.encode("utf-8")
23+
24+
for facet, tag_name in zip(facets, ["AdoptDontShop", "Boston", "DogsOfBluesky"]):
25+
start = facet["index"]["byteStart"]
26+
end = facet["index"]["byteEnd"]
27+
assert encoded[start:end] == f"#{tag_name}".encode("utf-8")
28+
assert facet["features"][0]["$type"] == "app.bsky.richtext.facet#tag"
29+
assert facet["features"][0]["tag"] == tag_name

0 commit comments

Comments
 (0)