a tool for shared writing and social publishing
at feature/footnotes 199 lines 4.9 kB view raw
1import { 2 LinkPreviewBody, 3 LinkPreviewImageResult, 4 LinkPreviewMetadataResult, 5} from "app/api/link_previews/route"; 6import { Replicache } from "replicache"; 7import type { ReplicacheMutators } from "src/replicache"; 8import { AtpAgent } from "@atproto/api"; 9import { v7 } from "uuid"; 10 11export async function addLinkBlock( 12 url: string, 13 entityID: string, 14 rep?: Replicache<ReplicacheMutators> | null, 15) { 16 if (!rep) return; 17 18 let res = await fetch("/api/link_previews", { 19 headers: { "Content-Type": "application/json" }, 20 method: "POST", 21 body: JSON.stringify({ url, type: "meta" } as LinkPreviewBody), 22 }); 23 if (res.status !== 200) { 24 await rep?.mutate.assertFact([ 25 { 26 entity: entityID, 27 attribute: "link/url", 28 data: { 29 type: "string", 30 value: url, 31 }, 32 }, 33 { 34 entity: entityID, 35 attribute: "block/type", 36 data: { type: "block-type-union", value: "link" }, 37 }, 38 ]); 39 return; 40 } 41 let data = await (res.json() as LinkPreviewMetadataResult); 42 if (!data.success) { 43 await rep?.mutate.assertFact([ 44 { 45 entity: entityID, 46 attribute: "link/url", 47 data: { 48 type: "string", 49 value: url, 50 }, 51 }, 52 { 53 entity: entityID, 54 attribute: "block/type", 55 data: { type: "block-type-union", value: "link" }, 56 }, 57 ]); 58 return; 59 } 60 61 if (data.data.links?.player?.[0]) { 62 let embed = data.data.links?.player?.[0]; 63 await rep.mutate.assertFact([ 64 { 65 entity: entityID, 66 attribute: "block/type", 67 data: { type: "block-type-union", value: "embed" }, 68 }, 69 { 70 entity: entityID, 71 attribute: "embed/url", 72 data: { 73 type: "string", 74 value: embed.href, 75 }, 76 }, 77 { 78 entity: entityID, 79 attribute: "embed/height", 80 data: { 81 type: "number", 82 value: embed.media?.height || 300, 83 }, 84 }, 85 ]); 86 return; 87 } 88 await rep?.mutate.assertFact([ 89 { 90 entity: entityID, 91 attribute: "link/url", 92 data: { 93 type: "string", 94 value: url, 95 }, 96 }, 97 { 98 entity: entityID, 99 attribute: "block/type", 100 data: { type: "block-type-union", value: "link" }, 101 }, 102 { 103 entity: entityID, 104 attribute: "link/title", 105 data: { 106 type: "string", 107 value: data.data.meta?.title || "", 108 }, 109 }, 110 { 111 entity: entityID, 112 attribute: "link/description", 113 data: { 114 type: "string", 115 value: data.data.meta?.description || "", 116 }, 117 }, 118 ]); 119 let imageRes = await fetch("/api/link_previews", { 120 headers: { "Content-Type": "application/json" }, 121 method: "POST", 122 body: JSON.stringify({ url, type: "image" } as LinkPreviewBody), 123 }); 124 125 let image_data = await (imageRes.json() as LinkPreviewImageResult); 126 127 await rep?.mutate.assertFact({ 128 entity: entityID, 129 attribute: "link/preview", 130 data: { 131 fallback: "", 132 type: "image", 133 src: image_data.url, 134 width: image_data.width, 135 height: image_data.height, 136 }, 137 }); 138} 139 140export async function addBlueskyPostBlock( 141 url: string, 142 entityID: string, 143 rep: Replicache<ReplicacheMutators>, 144) { 145 //construct bsky post uri from url 146 let urlParts = url?.split("/"); 147 let host = urlParts ? urlParts[2] : "bsky.app"; // "bsky.app", "blacksky.community", "witchsky.app", etc. 148 let userDidOrHandle = urlParts ? urlParts[4] : ""; // "schlage.town" or "did:plc:jjsc5rflv3cpv6hgtqhn2dcm" 149 let collection = "app.bsky.feed.post"; 150 let postId = urlParts ? urlParts[6] : ""; 151 let uri = `at://${userDidOrHandle}/${collection}/${postId}`; 152 153 let post = await getBlueskyPost(uri); 154 if (!post || post === undefined) return false; 155 156 await rep.mutate.assertFact([ 157 { 158 entity: entityID, 159 attribute: "block/type", 160 data: { type: "block-type-union", value: "bluesky-post" }, 161 }, 162 { 163 entity: entityID, 164 attribute: "block/bluesky-post", 165 data: { 166 type: "bluesky-post", 167 //TODO: this is a hack to get rid of a nested Array buffer which cannot be frozen, which replicache does on write. 168 value: JSON.parse(JSON.stringify(post.data.thread)), 169 }, 170 }, 171 { 172 entity: entityID, 173 attribute: "bluesky-post/host", 174 data: { 175 type: "string", 176 value: host, 177 }, 178 }, 179 ]); 180 return true; 181} 182async function getBlueskyPost(uri: string) { 183 const agent = new AtpAgent({ service: "https://public.api.bsky.app" }); 184 try { 185 let blueskyPost = await agent 186 .getPostThread({ 187 uri: uri, 188 depth: 0, 189 parentHeight: 0, 190 }) 191 .then((res) => { 192 return res; 193 }); 194 return blueskyPost; 195 } catch (error) { 196 let rect = document; 197 return; 198 } 199}