|
7 | 7 | description: 'The text content of the post. Supports markdown links [text](url) and hashtags.' |
8 | 8 | required: true |
9 | 9 | type: string |
| 10 | + embed_url: |
| 11 | + description: 'Optional URL to create a website card embed from (fetches OG tags)' |
| 12 | + required: false |
| 13 | + type: string |
10 | 14 | secrets: |
11 | 15 | BLUESKY_USERNAME: |
12 | 16 | required: true |
|
20 | 24 | - name: Post to BlueSky |
21 | 25 | env: |
22 | 26 | POST_INPUT: ${{ toJSON(inputs.post_text) }} |
| 27 | + EMBED_URL: ${{ inputs.embed_url }} |
23 | 28 | run: | |
24 | 29 | # Authenticate with BlueSky |
25 | 30 | echo "Authenticating with BlueSky..." |
@@ -111,26 +116,160 @@ jobs: |
111 | 116 | echo "Post text: $POST_TEXT" |
112 | 117 | echo "Facets: $FACETS" |
113 | 118 |
|
| 119 | + # Build embed if URL provided |
| 120 | + EMBED="" |
| 121 | + if [ -n "$EMBED_URL" ]; then |
| 122 | + echo "Fetching OG tags from: $EMBED_URL" |
| 123 | +
|
| 124 | + # Fetch page and extract OG tags using Python |
| 125 | + EMBED_DATA=$(python3 << 'PYEOF' |
| 126 | + import urllib.request |
| 127 | + import re |
| 128 | + import json |
| 129 | + import os |
| 130 | + import sys |
| 131 | +
|
| 132 | + url = os.environ.get('EMBED_URL', '') |
| 133 | + if not url: |
| 134 | + print(json.dumps({})) |
| 135 | + sys.exit(0) |
| 136 | +
|
| 137 | + try: |
| 138 | + req = urllib.request.Request(url, headers={'User-Agent': 'Mozilla/5.0'}) |
| 139 | + with urllib.request.urlopen(req, timeout=10) as response: |
| 140 | + html = response.read().decode('utf-8', errors='ignore') |
| 141 | +
|
| 142 | + # Extract OG tags |
| 143 | + og_title = re.search(r'<meta[^>]*property=["\']og:title["\'][^>]*content=["\']([^"\']+)["\']', html, re.I) |
| 144 | + og_title = og_title.group(1) if og_title else '' |
| 145 | +
|
| 146 | + og_desc = re.search(r'<meta[^>]*property=["\']og:description["\'][^>]*content=["\']([^"\']+)["\']', html, re.I) |
| 147 | + og_desc = og_desc.group(1) if og_desc else '' |
| 148 | +
|
| 149 | + og_image = re.search(r'<meta[^>]*property=["\']og:image["\'][^>]*content=["\']([^"\']+)["\']', html, re.I) |
| 150 | + og_image = og_image.group(1) if og_image else '' |
| 151 | +
|
| 152 | + print(json.dumps({'title': og_title, 'description': og_desc, 'image': og_image})) |
| 153 | + except Exception as e: |
| 154 | + print(json.dumps({'error': str(e)}), file=sys.stderr) |
| 155 | + print(json.dumps({})) |
| 156 | + PYEOF |
| 157 | + ) |
| 158 | +
|
| 159 | + OG_TITLE=$(echo "$EMBED_DATA" | jq -r '.title // empty') |
| 160 | + OG_DESC=$(echo "$EMBED_DATA" | jq -r '.description // empty') |
| 161 | + OG_IMAGE=$(echo "$EMBED_DATA" | jq -r '.image // empty') |
| 162 | +
|
| 163 | + echo "OG Title: $OG_TITLE" |
| 164 | + echo "OG Description: $OG_DESC" |
| 165 | + echo "OG Image: $OG_IMAGE" |
| 166 | +
|
| 167 | + if [ -n "$OG_TITLE" ]; then |
| 168 | + # Upload image as blob if present |
| 169 | + THUMB_JSON="" |
| 170 | + if [ -n "$OG_IMAGE" ]; then |
| 171 | + echo "Downloading image: $OG_IMAGE" |
| 172 | + curl -s -L -o /tmp/og_image "$OG_IMAGE" |
| 173 | +
|
| 174 | + if [ -f /tmp/og_image ] && [ -s /tmp/og_image ]; then |
| 175 | + # Detect mime type |
| 176 | + MIME_TYPE=$(file --mime-type -b /tmp/og_image) |
| 177 | + echo "Image MIME type: $MIME_TYPE" |
| 178 | +
|
| 179 | + # Upload blob |
| 180 | + echo "Uploading image blob..." |
| 181 | + BLOB_RESPONSE=$(curl -s -X POST https://bsky.social/xrpc/com.atproto.repo.uploadBlob \ |
| 182 | + -H "Content-Type: $MIME_TYPE" \ |
| 183 | + -H "Authorization: Bearer $ACCESS_TOKEN" \ |
| 184 | + --data-binary @/tmp/og_image) |
| 185 | +
|
| 186 | + BLOB_REF=$(echo "$BLOB_RESPONSE" | jq -c '.blob') |
| 187 | + if [ "$BLOB_REF" != "null" ] && [ -n "$BLOB_REF" ]; then |
| 188 | + echo "Blob uploaded successfully" |
| 189 | + THUMB_JSON="$BLOB_REF" |
| 190 | + else |
| 191 | + echo "Failed to upload blob: $BLOB_RESPONSE" |
| 192 | + fi |
| 193 | + fi |
| 194 | + fi |
| 195 | +
|
| 196 | + # Build embed object |
| 197 | + if [ -n "$THUMB_JSON" ]; then |
| 198 | + EMBED=$(jq -n \ |
| 199 | + --arg uri "$EMBED_URL" \ |
| 200 | + --arg title "$OG_TITLE" \ |
| 201 | + --arg desc "$OG_DESC" \ |
| 202 | + --argjson thumb "$THUMB_JSON" \ |
| 203 | + '{ |
| 204 | + "$type": "app.bsky.embed.external", |
| 205 | + "external": { |
| 206 | + "uri": $uri, |
| 207 | + "title": $title, |
| 208 | + "description": $desc, |
| 209 | + "thumb": $thumb |
| 210 | + } |
| 211 | + }') |
| 212 | + else |
| 213 | + EMBED=$(jq -n \ |
| 214 | + --arg uri "$EMBED_URL" \ |
| 215 | + --arg title "$OG_TITLE" \ |
| 216 | + --arg desc "$OG_DESC" \ |
| 217 | + '{ |
| 218 | + "$type": "app.bsky.embed.external", |
| 219 | + "external": { |
| 220 | + "uri": $uri, |
| 221 | + "title": $title, |
| 222 | + "description": $desc |
| 223 | + } |
| 224 | + }') |
| 225 | + fi |
| 226 | + echo "Embed created" |
| 227 | + fi |
| 228 | + fi |
| 229 | +
|
114 | 230 | # Create the post using jq to properly escape JSON |
115 | 231 | echo "Creating BlueSky post..." |
116 | | - POST_RESPONSE=$(jq -n \ |
117 | | - --arg did "$DID" \ |
118 | | - --arg text "$POST_TEXT" \ |
119 | | - --arg timestamp "$TIMESTAMP" \ |
120 | | - --argjson facets "$FACETS" \ |
121 | | - '{ |
122 | | - repo: $did, |
123 | | - collection: "app.bsky.feed.post", |
124 | | - record: { |
125 | | - text: $text, |
126 | | - facets: $facets, |
127 | | - createdAt: $timestamp, |
128 | | - "$type": "app.bsky.feed.post" |
129 | | - } |
130 | | - }' | curl -s -X POST https://bsky.social/xrpc/com.atproto.repo.createRecord \ |
131 | | - -H "Content-Type: application/json" \ |
132 | | - -H "Authorization: Bearer $ACCESS_TOKEN" \ |
133 | | - -d @-) |
| 232 | + if [ -n "$EMBED" ]; then |
| 233 | + POST_RESPONSE=$(jq -n \ |
| 234 | + --arg did "$DID" \ |
| 235 | + --arg text "$POST_TEXT" \ |
| 236 | + --arg timestamp "$TIMESTAMP" \ |
| 237 | + --argjson facets "$FACETS" \ |
| 238 | + --argjson embed "$EMBED" \ |
| 239 | + '{ |
| 240 | + repo: $did, |
| 241 | + collection: "app.bsky.feed.post", |
| 242 | + record: { |
| 243 | + text: $text, |
| 244 | + facets: $facets, |
| 245 | + embed: $embed, |
| 246 | + createdAt: $timestamp, |
| 247 | + "$type": "app.bsky.feed.post" |
| 248 | + } |
| 249 | + }' | curl -s -X POST https://bsky.social/xrpc/com.atproto.repo.createRecord \ |
| 250 | + -H "Content-Type: application/json" \ |
| 251 | + -H "Authorization: Bearer $ACCESS_TOKEN" \ |
| 252 | + -d @-) |
| 253 | + else |
| 254 | + POST_RESPONSE=$(jq -n \ |
| 255 | + --arg did "$DID" \ |
| 256 | + --arg text "$POST_TEXT" \ |
| 257 | + --arg timestamp "$TIMESTAMP" \ |
| 258 | + --argjson facets "$FACETS" \ |
| 259 | + '{ |
| 260 | + repo: $did, |
| 261 | + collection: "app.bsky.feed.post", |
| 262 | + record: { |
| 263 | + text: $text, |
| 264 | + facets: $facets, |
| 265 | + createdAt: $timestamp, |
| 266 | + "$type": "app.bsky.feed.post" |
| 267 | + } |
| 268 | + }' | curl -s -X POST https://bsky.social/xrpc/com.atproto.repo.createRecord \ |
| 269 | + -H "Content-Type: application/json" \ |
| 270 | + -H "Authorization: Bearer $ACCESS_TOKEN" \ |
| 271 | + -d @-) |
| 272 | + fi |
134 | 273 |
|
135 | 274 | POST_URI=$(echo "$POST_RESPONSE" | jq -r '.uri') |
136 | 275 |
|
|
0 commit comments