An HTML-only Bluesky frontend

update bot handling, minor changes to actor and thread pages

+97 -11
+1 -1
actor.js
··· 142 142 case "app.bsky.embed.external": 143 143 // TODO: external embed 144 144 default: 145 - embeds.push(`<pre>Missing embed type ${embedType}; <a href="https://todo.sr.ht/~jordanreger/htmlsky">please make an issue</a>.</pre>`); 145 + embeds.push(`<p>Missing embed type ${embedType}; <a href="https://todo.sr.ht/~jordanreger/htmlsky">please make an issue</a>.</p>`); 146 146 break; 147 147 } 148 148 }
+4 -2
main.js
··· 6 6 7 7 export const agent = new AtpAgent({ service: "https://public.api.bsky.app" }); 8 8 9 - Deno.serve(async (req) => { 9 + const controller = new AbortController(), signal = controller.signal; 10 + Deno.serve({ signal }, async (req, info) => { 10 11 // TEMPORARY: FOR BOT DETECTION ONLY 11 12 const headers = new Headers(req.headers); 12 13 const ua = headers.get("user-agent"); 13 14 14 15 console.log(ua); 15 16 17 + // Bots should receive empty reply 16 18 if ( 17 19 ua.includes("amazon") || 18 20 ua.includes("facebook") || 19 21 ua.includes("Bytespider") || 20 22 ua.includes("bot") 21 23 ) { 22 - return new Response(null, { status: 403 }); 24 + controller.abort(); 23 25 } 24 26 25 27 const url = new URL(req.url);
+92 -8
thread.js
··· 1 - import { agent, getRelativeDate, DateTimeFormat } from "./main.js"; 1 + import { getRelativeDate, DateTimeFormat } from "./main.js"; 2 2 import { getFacets } from "./facets.js"; 3 3 4 4 export default class Thread { ··· 9 9 } 10 10 11 11 Post() { 12 + let res = ""; 13 + 12 14 const thread = this.thread; 13 15 const post = thread.post; 14 16 const author = post.author; ··· 18 20 author.handle = author.handle !== "handle.invalid" ? author.handle : author.did; 19 21 author.avatar = author.avatar ? author.avatar : "/static/avatar.jpg"; 20 22 21 - 22 - const text = record.text ? `<p>${getFacets(record.text, record.facets)}</p>` : ""; 23 - 24 - return ` 23 + res += ` 25 24 <head> 26 25 <meta name="color-scheme" content="light dark"> 27 26 <meta name="viewport" content="width=device-width, initial-scale=1"> 28 27 <title>${author.displayName}: ${post.record.text} &#8212; HTMLsky</title> 29 28 </head> 29 + `; 30 + 31 + if (thread.parent) { 32 + const parent = thread.parent; 33 + const post = parent.post; 34 + 35 + const rkey = post.uri.split("/").at(-1); 36 + if (post.notFound || post.blocked) { 37 + res +=` 38 + <tr><td> 39 + <table> 40 + <tr><td> 41 + Post not found. 42 + </td></tr> 43 + </table> 44 + </td></tr> 45 + `; 46 + } else { 47 + post.author.displayName = post.author.displayName ? post.author.displayName : post.author.handle; 48 + post.author.handle = post.author.handle !== "handle.invalid" ? post.author.handle : post.author.did; 49 + 50 + const text = post.record.text ? `<p>${getFacets(post.record.text, post.record.facets)}</p>` : ""; 51 + 52 + res += ` 53 + <tr> 54 + <td> 55 + <table> 56 + <tr><td> 57 + <b>${post.author.displayName}</b><br><a href="/profile/${post.author.handle}/">@${post.author.handle}</a> 58 + / <a href="/profile/${post.author.handle}/post/${rkey}/">${rkey}</a> 59 + </td></tr> 60 + <tr><td> 61 + ${text} 62 + </td></tr> 63 + <tr><td> 64 + <b>${post.replyCount}</b> replies &middot; 65 + <b>${post.repostCount}</b> reposts &middot; 66 + <b>${post.likeCount}</b> likes 67 + &mdash; 68 + <time title="${DateTimeFormat.format(new Date(post.record.createdAt))}"><i>${getRelativeDate(new Date(post.record.createdAt))}</i></time> 69 + </td></tr> 70 + </table> 71 + </td> 72 + </tr> 73 + <hr> 74 + `; 75 + } 76 + } 77 + 78 + const text = record.text ? `<p>${getFacets(record.text, record.facets)}</p>` : ""; 79 + 80 + const embeds = []; 81 + if (record.embed) { 82 + const embedType = record.embed["$type"]; 83 + 84 + switch(embedType) { 85 + case "app.bsky.embed.images": 86 + embeds.push(`<ul>`); 87 + for (const image of record.embed.images) { 88 + // TODO: have a separate page for images with alt text and stuff? 89 + const embedURL = `https://cdn.bsky.app/img/feed_fullsize/plain/${author.did}/${image.image.ref}@${image.image.mimeType.split("/")[1]}`; 90 + embeds.push(`<li><a href="${embedURL}">${image.alt ? image.alt : "image"}</a></li>`); 91 + } 92 + embeds.push(`</ul>`); 93 + break; 94 + case "app.bsky.embed.record": 95 + // TODO: record embed 96 + break; 97 + case "app.bsky.embed.recordWithMedia": 98 + // TODO: recordWithMedia embed 99 + break; 100 + case "app.bsky.embed.external": 101 + // TODO: external embed 102 + break; 103 + default: 104 + embeds.push(`<p>Missing embed type ${embedType}; <a href="https://todo.sr.ht/~jordanreger/htmlsky">please make an issue</a>.</p>`); 105 + break; 106 + } 107 + } 108 + 109 + res += ` 30 110 <table> 31 111 <tr> 32 112 <td valign="top" height="45" width="45"> ··· 42 122 <tr><td colspan="2"> 43 123 ${text} 44 124 </td></tr> 45 - <tr><td colspan="2">&nbsp;</td></tr> 125 + <tr><td colspan="2"> 126 + ${post.embed ? embeds.join("\n") : ``} 127 + </td></tr> 46 128 <tr><td colspan="2"> 47 129 <b>${post.replyCount}</b> replies &middot; 48 130 <b>${post.repostCount}</b> reposts &middot; ··· 53 135 </table> 54 136 <hr> 55 137 `; 138 + 139 + return res; 56 140 } 57 141 58 142 async Replies(prevCursor) { ··· 78 162 for (const image of record.embed.images) { 79 163 // TODO: have a separate page for images with alt text and stuff? 80 164 const embedURL = `https://cdn.bsky.app/img/feed_fullsize/plain/${author.did}/${image.image.ref}@${image.image.mimeType.split("/")[1]}`; 81 - embeds.push(`<li><a href="${embedURL}">${image.alt}</a></li>`); 165 + embeds.push(`<li><a href="${embedURL}">${image.alt ? image.alt : "image"}</a></li>`); 82 166 } 83 167 embeds.push(`</ul>`); 84 168 break; ··· 92 176 // TODO: external embed 93 177 break; 94 178 default: 95 - embeds.push(`<pre>Missing embed type ${embedType}; <a href="https://todo.sr.ht/~jordanreger/htmlsky">please make an issue</a>.</pre>`); 179 + embeds.push(`<p>Missing embed type ${embedType}; <a href="https://todo.sr.ht/~jordanreger/htmlsky">please make an issue</a>.</p>`); 96 180 break; 97 181 } 98 182 }