Various AT Protocol integrations with obsidian
20
fork

Configure Feed

Select the types of activity you want to include in your feed.

add at/did mention facets for leaflet

+116 -17
+43 -17
src/lib/markdown/leaflet.ts
··· 10 10 PubLeafletRichtextFacet, 11 11 } from "@atcute/leaflet"; 12 12 import { parseMarkdown, cleanPlaintext } from "../markdown"; 13 + import { UrlToActorIdentifier, UrlToRecordUri, resolveHandleInAtUri } from "../../util"; 14 + import { resolveActor } from "lib/identity"; 13 15 14 16 15 17 const textEncoder = new TextEncoder(); ··· 34 36 }; 35 37 } 36 38 37 - function buildTextFromNodes(nodes: RootContent[]): { 39 + async function buildTextFromNodes(nodes: RootContent[]): Promise<{ 38 40 text: string; 39 41 facets: PubLeafletRichtextFacet.Main[]; 40 - } { 42 + }> { 41 43 let text = ""; 42 44 let byteOffset = 0; 43 45 const facets: PubLeafletRichtextFacet.Main[] = []; 46 + const pending: Promise<void>[] = []; 44 47 45 48 const appendText = (value: string) => { 46 49 if (!value) return; ··· 94 97 for (const child of node.children) walk(child); 95 98 const end = byteOffset; 96 99 if (start < end && node.url) { 97 - facets.push(createFacet(start, end, [{ $type: "pub.leaflet.richtext.facet#link", uri: node.url }])); 100 + const recordUri = UrlToRecordUri(node.url); 101 + const actorIdentifier = UrlToActorIdentifier(node.url); 102 + 103 + if (recordUri) { 104 + pending.push( 105 + resolveHandleInAtUri(recordUri).then(resolvedUri => { 106 + if (!resolvedUri) return; 107 + facets.push(createFacet(start, end, [ 108 + { $type: "pub.leaflet.richtext.facet#atMention", atURI: resolvedUri }, 109 + ])); 110 + }) 111 + ); 112 + } else if (actorIdentifier) { 113 + pending.push( 114 + resolveActor(actorIdentifier).then(actor => { 115 + if (!actor) return; 116 + facets.push(createFacet(start, end, [ 117 + { $type: "pub.leaflet.richtext.facet#didMention", did: actor.did }, 118 + ])); 119 + }) 120 + ); 121 + } else { 122 + facets.push(createFacet(start, end, [{ $type: "pub.leaflet.richtext.facet#link", uri: node.url }])); 123 + } 98 124 } 99 125 return; 100 126 } ··· 115 141 }; 116 142 117 143 for (const node of nodes) walk(node); 118 - 144 + await Promise.all(pending); 119 145 return { text, facets }; 120 146 } 121 147 122 - function buildTextBlock(node: { children?: RootContent[] }): PubLeafletBlocksText.Main & { $type: "pub.leaflet.blocks.text" } { 123 - const { text, facets } = buildTextFromNodes(node.children ?? []); 148 + async function buildTextBlock(node: { children?: RootContent[] }): Promise<PubLeafletBlocksText.Main & { $type: "pub.leaflet.blocks.text" }> { 149 + const { text, facets } = await buildTextFromNodes(node.children ?? []); 124 150 return { 125 151 $type: "pub.leaflet.blocks.text" as const, 126 152 plaintext: text, ··· 129 155 }; 130 156 } 131 157 132 - export function markdownToLeafletContent(markdown: string): PubLeafletContent.Main { 158 + export async function markdownToLeafletContent(markdown: string): Promise<PubLeafletContent.Main> { 133 159 const tree = parseMarkdown(markdown); 134 160 const blocks: PubLeafletPagesLinearDocument.Block[] = []; 135 161 136 162 for (const node of tree.children) { 137 - const block = convertNodeToBlock(node); 163 + const block = await convertNodeToBlock(node); 138 164 if (block) blocks.push(block); 139 165 } 140 166 ··· 148 174 return record as PubLeafletContent.Main; 149 175 } 150 176 151 - function convertListItem(item: ListItem): PubLeafletBlocksUnorderedList.ListItem { 177 + async function convertListItem(item: ListItem): Promise<PubLeafletBlocksUnorderedList.ListItem> { 152 178 const textChildren: RootContent[] = []; 153 179 const nestedLists: RootContent[] = []; 154 180 ··· 163 189 // handle text content and nested lists separately 164 190 const result: PubLeafletBlocksUnorderedList.ListItem = { 165 191 $type: "pub.leaflet.blocks.unorderedList#listItem", 166 - content: buildTextBlock({ children: textChildren }), 192 + content: await buildTextBlock({ children: textChildren }), 167 193 }; 168 194 169 195 if (nestedLists.length > 0) { 170 - result.children = nestedLists.flatMap((list) => 171 - (list as List).children.map(convertListItem) 196 + result.children = await Promise.all( 197 + nestedLists.flatMap((list) => (list as List).children.map(convertListItem)) 172 198 ); 173 199 } 174 200 175 201 return result; 176 202 } 177 203 178 - function convertNodeToBlock(node: RootContent): PubLeafletPagesLinearDocument.Block | null { 204 + async function convertNodeToBlock(node: RootContent): Promise<PubLeafletPagesLinearDocument.Block | null> { 179 205 switch (node.type) { 180 206 case "heading": { 181 - const { text, facets } = buildTextFromNodes(node.children); 207 + const { text, facets } = await buildTextFromNodes(node.children); 182 208 return { 183 209 block: { 184 210 $type: "pub.leaflet.blocks.header", ··· 192 218 193 219 case "paragraph": 194 220 return { 195 - block: buildTextBlock(node), 221 + block: await buildTextBlock(node), 196 222 alignment: "pub.leaflet.pages.linearDocument#textAlignLeft", 197 223 } as PubLeafletPagesLinearDocument.Block; 198 224 ··· 200 226 return { 201 227 block: { 202 228 $type: "pub.leaflet.blocks.unorderedList", 203 - children: node.children.map(convertListItem), 229 + children: await Promise.all(node.children.map(convertListItem)), 204 230 }, 205 231 alignment: "pub.leaflet.pages.linearDocument#textAlignLeft", 206 232 }; ··· 223 249 }; 224 250 225 251 case "blockquote": { 226 - const { text, facets } = buildTextFromNodes( 252 + const { text, facets } = await buildTextFromNodes( 227 253 node.children.flatMap((c) => ("children" in c ? c.children : []) as RootContent[]) 228 254 ); 229 255 return {
+73
src/util.ts
··· 1 1 import { requestUrl } from "obsidian"; 2 + import { ResourceUri } from "@atcute/lexicons"; 3 + import { ActorIdentifier, isActorIdentifier, isDid, parseResourceUri } from "@atcute/lexicons/syntax"; 4 + import { resolveActor } from "lib/identity"; 2 5 3 6 const imageCache = new Map<string, string>(); 4 7 ··· 32 35 return undefined; 33 36 } 34 37 } 38 + 39 + export const BSKY_POST_RE = /https:\/\/bsky\.app\/profile\/([^/?#]+)\/post\/([A-Za-z0-9]+)/; 40 + 41 + export function bskyPostATUri(url: string): ResourceUri | null { 42 + const match = url.match(BSKY_POST_RE); 43 + if (!match) return null; 44 + 45 + const [, handleOrDid, rkey] = match; 46 + if (!handleOrDid || !rkey) return null; 47 + 48 + return `at://${handleOrDid}/app.bsky.feed.post/${rkey}` 49 + } 50 + 51 + export function UrlToRecordUri(url: string): ResourceUri | null { 52 + // Already an AT URI with a record key 53 + const parsed = parseResourceUri(url); 54 + if (parsed.ok && parsed.value.rkey) { 55 + return url as ResourceUri; 56 + } 57 + 58 + // bsky.app post URL 59 + const match = url.match(BSKY_POST_RE); 60 + if (!match) return null; 61 + 62 + const [, handleOrDid, rkey] = match; 63 + if (!handleOrDid || !rkey) return null; 64 + 65 + return `at://${handleOrDid}/app.bsky.feed.post/${rkey}` as ResourceUri; 66 + } 67 + 68 + /** 69 + * Given an AT URI that may contain a handle, resolve the handle to a DID 70 + * and return the canonical AT URI 71 + */ 72 + export async function resolveHandleInAtUri(uri: ResourceUri): Promise<ResourceUri| null> { 73 + const parsed = parseResourceUri(uri); 74 + if (!parsed.ok) return uri; 75 + 76 + const { repo, collection, rkey } = parsed.value; 77 + if (isDid(repo)) return uri; 78 + 79 + try { 80 + const actor = await resolveActor(repo as ActorIdentifier); 81 + if (collection && rkey) { 82 + return `at://${actor.did}/${collection}/${rkey}` as ResourceUri; 83 + } 84 + return `at://${actor.did}` as ResourceUri; 85 + } catch { 86 + return null; 87 + } 88 + } 89 + 90 + export function UrlToActorIdentifier(url: string): ActorIdentifier | null { 91 + const parsed = parseResourceUri(url); 92 + if (parsed.ok) { 93 + return parsed.value.repo 94 + } 95 + if (isActorIdentifier(url)) { 96 + return url; 97 + } 98 + const profileMatch = url.match(BSKY_PROFILE_RE); 99 + if (!profileMatch) return null; 100 + 101 + const [, handleOrDid] = profileMatch; 102 + if (isActorIdentifier(handleOrDid)) { 103 + return handleOrDid; 104 + } 105 + return null; 106 + } 107 +