a tool for shared writing and social publishing
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}