Skip to content

Commit 7017c12

Browse files
committed
Action: Add social media cards to BlueSky posts.
1 parent 4c433d2 commit 7017c12

File tree

3 files changed

+65
-23
lines changed

3 files changed

+65
-23
lines changed
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
twitter_secrets.py
2+
bluesky_secrets.py

.github/actions/tweet-commit/post_commit.py

Lines changed: 56 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@
1616

1717
from git import Repo
1818
import tweepy
19-
from atproto import Client, client_utils
19+
from atproto import Client, client_utils, models
20+
import httpx
2021

2122

2223
# Getting the Twitter secrets form local dev file or GH action secrets
@@ -27,10 +28,6 @@
2728
TWITTER_ACCESS_TOKEN,
2829
TWITTER_ACCESS_TOKEN_SECRET,
2930
)
30-
from bluesky_secrets import (
31-
BLUESKY_USERNAME,
32-
BLUESKY_TOKEN,
33-
)
3431
except ImportError:
3532
TWITTER_CONSUMER_KEY = os.environ.get("INPUT_TWITTER_CONSUMER_KEY", None)
3633
TWITTER_CONSUMER_SECRET = os.environ.get(
@@ -40,6 +37,12 @@
4037
TWITTER_ACCESS_TOKEN_SECRET = os.environ.get(
4138
"INPUT_TWITTER_ACCESS_TOKEN_SECRET", None
4239
)
40+
try:
41+
from bluesky_secrets import (
42+
BLUESKY_USERNAME,
43+
BLUESKY_TOKEN,
44+
)
45+
except ImportError:
4346
BLUESKY_USERNAME = os.environ.get("INPUT_BLUESKY_USERNAME", None)
4447
BLUESKY_TOKEN = os.environ.get("INPUT_BLUESKY_TOKEN", None)
4548

@@ -181,7 +184,7 @@ def format_msg_bluesky(section, title, url, description):
181184
description = format_use_hashtags(description)
182185

183186
# Now let's make sure we don't exceed the max character limit
184-
msg = "{}\n\n{}\n{}".format(section, title, description)
187+
msg = "{} - {}\n\n{}".format(section, title, description)
185188
if len(msg) > BLUESKY_MAX_CHARS:
186189
ellipsis = "..."
187190
characters_over = len(msg) - BLUESKY_MAX_CHARS + len(ellipsis)
@@ -190,20 +193,62 @@ def format_msg_bluesky(section, title, url, description):
190193
)
191194

192195
text_builder = client_utils.TextBuilder()
193-
text_builder.text(section + "\n\n")
196+
text_builder.text(section + " - ")
194197
text_builder.link(title, url)
195-
text_builder.text("\n" + description)
198+
text_builder.text("\n\n" + description)
196199
return text_builder
197200

198201

199-
def skeet_msg(text_builder):
202+
def skeet_msg(text_builder, url):
200203
"""Post to BluSky the given message content."""
201204
if not all((BLUESKY_USERNAME, BLUESKY_TOKEN)):
202205
print("BlueSky username or token not available.")
203206
sys.exit(1)
207+
208+
# Posting Open Graph Protocol (OGP) social media cards, based on example:
209+
# https://github.com/MarshalX/atproto/blob/v0.0.56/examples/advanced_usage/send_ogp_link_card.py
210+
_META_PATTERN = re.compile(r'<meta property="og:.*?>')
211+
_CONTENT_PATTERN = re.compile(r'<meta[^>]+content="([^"]+)"')
212+
213+
def _get_og_tag_value(og_tags, tag_name):
214+
# tag = _find_tag(og_tags, tag_name)
215+
for tag in og_tags:
216+
if tag_name in tag:
217+
match = _CONTENT_PATTERN.match(tag)
218+
if match:
219+
return match.group(1)
220+
return None
221+
222+
def _get_og_tags(url):
223+
response = httpx.get(url)
224+
response.raise_for_status()
225+
og_tags = _META_PATTERN.findall(response.text)
226+
og_image = _get_og_tag_value(og_tags, "og:image")
227+
og_title = _get_og_tag_value(og_tags, "og:title")
228+
og_description = _get_og_tag_value(og_tags, "og:description")
229+
return og_image, og_title, og_description
230+
204231
client = Client()
205232
client.login(BLUESKY_USERNAME, BLUESKY_TOKEN)
206-
client.send_post(text_builder)
233+
234+
# Process social media card
235+
img_url, title, description = _get_og_tags(url)
236+
if title and description:
237+
thumb_blob = None
238+
if img_url:
239+
# Download image from og:image url and upload it as a blob
240+
img_data = httpx.get(img_url).content
241+
thumb_blob = client.upload_blob(img_data).blob
242+
243+
# AppBskyEmbedExternal is the same as "link card" in the app
244+
embed_external = models.AppBskyEmbedExternal.Main(
245+
external=models.AppBskyEmbedExternal.External(
246+
title=title, description=description, uri=url, thumb=thumb_blob
247+
)
248+
)
249+
client.send_post(text=text_builder, embed=embed_external)
250+
else:
251+
client.send_post(text_builder)
207252

208253

209254
def main():
@@ -239,7 +284,7 @@ def main():
239284
flush=True,
240285
)
241286
tweet_msg(formatted_tweet)
242-
skeet_msg(formatted_skeet)
287+
skeet_msg(formatted_skeet, entry["url"])
243288
print("Sent Tweet and Skeet #{}!".format(i))
244289

245290

.github/actions/tweet-commit/tests.py

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -62,8 +62,7 @@ def test_commit_1(self):
6262
)
6363
self.assertEqual(
6464
skeet.build_text(),
65-
"MicroPython Libraries\n\n"
66-
"MB1013\n"
65+
"MicroPython Libraries - MB1013\n\n"
6766
"Module for the MB1013 ultrasonic sensor controlled via UART.",
6867
)
6968

@@ -197,8 +196,7 @@ def test_commit_6(self):
197196
)
198197
self.assertEqual(
199198
skeet.build_text(),
200-
"📱 Mobile Apps\n\n"
201-
"Official Swift Playgrounds\n"
199+
"📱 Mobile Apps - Official Swift Playgrounds\n\n"
202200
"([Source Code](https://github.com/microbit-foundation/"
203201
"microbit-swift-playgrounds)) "
204202
"Swift Playgrounds is an app for the iPad that helps teach people "
@@ -327,7 +325,7 @@ def test_msg_format_max_length(self):
327325
"s" * 9, "t" * 12, "u" * 23, ("d" * 230) + "."
328326
)
329327
skeet = post_commit.format_msg_bluesky(
330-
"s" * 8, "t" * 9, "u" * 100, ("d" * 279) + "."
328+
"s" * 7, "t" * 8, "u" * 100, ("d" * 279) + "."
331329
)
332330

333331
self.assertEqual(len(tweet), 280)
@@ -343,7 +341,7 @@ def test_msg_format_max_length(self):
343341
self.assertEqual(len(skeet.build_text()), 300)
344342
self.assertEqual(
345343
skeet.build_text(),
346-
("s" * 8) + "\n\n" + ("t" * 9) + "\n" + ("d" * 279) + ".",
344+
("s" * 7) + " - " + ("t" * 8) + "\n\n" + ("d" * 279) + ".",
347345
)
348346

349347
def test_msg_format_over_length(self):
@@ -357,7 +355,7 @@ def test_msg_format_over_length(self):
357355
"s" * 9, "t" * 12, "u" * 23, "dd " * 1000
358356
)
359357
skeet = post_commit.format_msg_bluesky(
360-
"s" * 8, "t" * 9, "u" * 100, "dd " * 1000
358+
"s" * 7, "t" * 8, "u" * 100, "dd " * 1000
361359
)
362360

363361
self.assertEqual(len(tweet), 279)
@@ -374,7 +372,7 @@ def test_msg_format_over_length(self):
374372
self.assertEqual(len(skeet.build_text()), 298)
375373
self.assertEqual(
376374
skeet.build_text(),
377-
("s" * 8) + "\n\n" + ("t" * 9) + "\n" + ("dd " * 91) + "dd...",
375+
("s" * 7) + " - " + ("t" * 8) + "\n\n" + ("dd " * 91) + "dd...",
378376
)
379377

380378
def test_long_tweet(self):
@@ -419,8 +417,7 @@ def test_long_tweet(self):
419417
)
420418
self.assertEqual(
421419
skeet.build_text(),
422-
"Miscellaneous\n\n"
423-
"Radiobit, a BBC Micro:Bit RF firmware\n"
420+
"Miscellaneous - Radiobit, a BBC Micro:Bit RF firmware\n\n"
424421
"Radiobit is composed of a dedicated #MicroPython-based firmware "
425422
"and a set of tools allowing security researchers to sniff, "
426423
"receive and send data over Nordic's ShockBurst protocol, "
@@ -693,8 +690,7 @@ def test_commit_replace_makecode_2(self):
693690
)
694691
self.assertEqual(
695692
skeet.build_text(),
696-
"MakeCode Libraries\n\n"
697-
"CCS811\n"
693+
"MakeCode Libraries - CCS811\n\n"
698694
"#MakeCode Package for the CCS811 Air Quality Sensor.",
699695
)
700696

0 commit comments

Comments
 (0)