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