Various AT Protocol integrations with obsidian
20
fork

Configure Feed

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

Use slingshot for resolving actors, faster rendering of publications (#7)

* resolve actors with slingshot

* progressively render publiactions

* bump version

authored by

treethought and committed by
GitHub
b4c8247e 77cc0f9a

+95 -20
+3
bun.lock
··· 10 10 "@atcute/client": "^4.2.1", 11 11 "@atcute/identity-resolver": "^1.2.2", 12 12 "@atcute/leaflet": "^1.0.17", 13 + "@atcute/microcosm": "^1.0.1", 13 14 "@atcute/oauth-browser-client": "^2.0.3", 14 15 "@atcute/pckt": "^0.1.5", 15 16 "@atcute/standard-site": "^1.0.0", ··· 60 61 "@atcute/lexicon-resolver": ["@atcute/lexicon-resolver@0.1.6", "", { "dependencies": { "@atcute/crypto": "^2.3.0", "@atcute/lexicon-doc": "^2.0.6", "@atcute/lexicons": "^1.2.6", "@atcute/repo": "^0.1.1", "@atcute/util-fetch": "^1.0.5", "@badrap/valita": "^0.4.6" }, "peerDependencies": { "@atcute/identity": "^1.1.0", "@atcute/identity-resolver": "^1.1.3" } }, "sha512-wJC/ChmpP7k+ywpOd07CMvioXjIGaFpF3bDwXLi/086LYjSWHOvtW6pyC+mqP5wLhjyH2hn4wmi77Buew1l1aw=="], 61 62 62 63 "@atcute/lexicons": ["@atcute/lexicons@1.2.7", "", { "dependencies": { "@atcute/uint8array": "^1.1.0", "@atcute/util-text": "^1.1.0", "@standard-schema/spec": "^1.1.0", "esm-env": "^1.2.2" } }, "sha512-gCvkSMI1F1zx7xXa59iPiSKMH3L5Hga6iurGqQjaQbE2V/np/2QuDqQzt96TNbWfaFAXE9f9oY+0z3ljf/bweA=="], 64 + 65 + "@atcute/microcosm": ["@atcute/microcosm@1.0.1", "", { "dependencies": { "@atcute/lexicons": "^1.2.7" } }, "sha512-siyreLgOCZ6gT3x5tajTw1MrlR0s4SDNlUvaRYQZrAUZS1xuuLx1Ko/cwsf+/QQzEN6K1wgtTC0J6HqtRZwWVg=="], 63 66 64 67 "@atcute/mst": ["@atcute/mst@0.1.2", "", { "dependencies": { "@atcute/cbor": "^2.3.0", "@atcute/cid": "^2.4.0", "@atcute/uint8array": "^1.0.6" } }, "sha512-Oz5CZTjqauEJLT9B+zkoy/mjl216DrjCxJFrguRV3N+1NkIbCfAcSRf3UDSNjfzDzBkJvC1WjA/3oQkm83duPg=="], 65 68
+1 -1
manifest.json
··· 1 1 { 2 2 "id": "atmosphere", 3 3 "name": "Atmosphere", 4 - "version": "0.1.9", 4 + "version": "0.1.10", 5 5 "minAppVersion": "0.15.0", 6 6 "description": "Various integrations with AT Protocol.", 7 7 "author": "treethought",
+2 -1
package.json
··· 1 1 { 2 2 "name": "obsidian-atmosphere", 3 - "version": "0.1.9", 3 + "version": "0.1.10", 4 4 "description": "Various integrations with AT Protocol.", 5 5 "main": "main.js", 6 6 "type": "module", ··· 30 30 "@atcute/client": "^4.2.1", 31 31 "@atcute/identity-resolver": "^1.2.2", 32 32 "@atcute/leaflet": "^1.0.17", 33 + "@atcute/microcosm": "^1.0.1", 33 34 "@atcute/oauth-browser-client": "^2.0.3", 34 35 "@atcute/pckt": "^0.1.5", 35 36 "@atcute/standard-site": "^1.0.0",
+1
src/env.d.ts
··· 2 2 /// <reference types="@atcute/atproto" /> 3 3 /// <reference types="@atcute/standard-site" /> 4 4 /// <reference types="@atcute/leaflet" /> 5 + /// <reference types="@atcute/microcosm" />
+12 -4
src/lib/client.ts
··· 5 5 6 6 const DEFAULT_SERVICE = "https://bsky.social"; 7 7 8 + // Custom fetch function using Obsidian's requestUrl to avoid CORS issues 8 9 export interface Credentials { 9 10 identifier: string; 10 11 password: string; ··· 12 13 13 14 export class ATClient extends Client { 14 15 hh: Handler; 16 + slingshot: Client 15 17 16 18 constructor(creds?: Credentials) { 17 19 const handler = new Handler(creds); ··· 26 28 return this.hh.cm.session; 27 29 } 28 30 29 - getActor(identifier: string): Promise<ResolvedActor> { 31 + async getActor(identifier: string): Promise<ResolvedActor> { 30 32 return this.hh.getActor(identifier); 31 33 } 32 34 } ··· 49 51 return cached; 50 52 } 51 53 if (isActorIdentifier(identifier)) { 52 - const res = await resolveActor(identifier); 53 - this.cache.set(key, res); 54 - return res; 54 + try { 55 + const res = await resolveActor(identifier); 56 + this.cache.set(key, res); 57 + return res; 58 + } 59 + catch (e) { 60 + console.error("Error resolving actor:", e) 61 + throw new Error("Failed to resolve actor: " + JSON.stringify(identifier)); 62 + } 55 63 } else { 56 64 throw new Error("Invalid actor identifier: " + JSON.stringify(identifier)); 57 65 }
+60 -2
src/lib/identity.ts
··· 4 4 DohJsonHandleResolver, 5 5 LocalActorResolver, 6 6 WellKnownHandleResolver, 7 + type ActorResolver, 8 + type ResolveActorOptions, 9 + type ResolvedActor, 7 10 } from '@atcute/identity-resolver'; 8 11 9 12 import { ··· 11 14 PlcDidDocumentResolver, 12 15 WebDidDocumentResolver, 13 16 } from '@atcute/identity-resolver'; 17 + 18 + import { Client, ok, simpleFetchHandler } from "@atcute/client"; 19 + 14 20 15 21 const handleResolver = new CompositeHandleResolver({ 16 22 methods: { ··· 26 32 }, 27 33 }); 28 34 29 - const actorResolver = new LocalActorResolver({ 35 + const localActorResolver = new LocalActorResolver({ 30 36 handleResolver, 31 37 didDocumentResolver: didResolver, 32 38 }); 33 39 40 + export class SlingshotActorResolver implements ActorResolver { 41 + private client: Client; 42 + 43 + constructor() { 44 + this.client = new Client({ 45 + handler: simpleFetchHandler({ 46 + service: "https://slingshot.microcosm.blue", 47 + // fetch: obsidianFetch 48 + }) 49 + }); 50 + } 51 + 52 + async resolve(actor: ActorIdentifier, options?: ResolveActorOptions): Promise<ResolvedActor> { 53 + const resolved = await ok( 54 + this.client.get("blue.microcosm.identity.resolveMiniDoc", { 55 + params: { identifier: actor }, 56 + signal: options?.signal, 57 + }) 58 + ); 59 + 60 + return { 61 + did: resolved.did, 62 + handle: resolved.handle, 63 + pds: resolved.pds, 64 + }; 65 + } 66 + } 67 + 68 + class CompositeActorResolver implements ActorResolver { 69 + private slingshotResolver: SlingshotActorResolver; 70 + private localResolver: LocalActorResolver; 71 + 72 + constructor(slingshotResolver: SlingshotActorResolver, localResolver: LocalActorResolver) { 73 + this.slingshotResolver = slingshotResolver; 74 + this.localResolver = localResolver; 75 + } 76 + 77 + async resolve(actor: ActorIdentifier, options?: ResolveActorOptions): Promise<ResolvedActor> { 78 + try { 79 + const resolved = await this.slingshotResolver.resolve(actor, options); 80 + return resolved; 81 + } catch (error) { 82 + console.warn("Slingshot actor resolution failed, falling back to local resolver:", error); 83 + return await this.localResolver.resolve(actor, options); 84 + } 85 + } 86 + } 87 + 88 + const slingshotResolver = new SlingshotActorResolver(); 89 + 90 + const compositeResolver = new CompositeActorResolver(slingshotResolver, localActorResolver); 91 + 34 92 export async function resolveActor(identifier: ActorIdentifier) { 35 - return actorResolver.resolve(identifier); 93 + return compositeResolver.resolve(identifier); 36 94 } 37 95 38 96
+16 -12
src/views/standardfeed.ts
··· 1 - import { getSubscribedPublications } from "lib/standardsite"; 1 + import { getSubscriptions, getPublication, getPublicationDocuments } from "lib/standardsite"; 2 2 import AtmospherePlugin from "main"; 3 3 import { ItemView, Notice, WorkspaceLeaf, setIcon } from "obsidian"; 4 4 import { Main as Document } from "@atcute/standard-site/types/document"; 5 5 import { Main as Publication } from "@atcute/standard-site/types/publication"; 6 6 import { ATRecord } from "lib"; 7 7 import { parseResourceUri } from "@atcute/lexicons"; 8 - import { getPublicationDocuments } from "lib/standardsite"; 9 8 10 9 export const VIEW_ATMOSPHERE_STANDARD_FEED = "atmosphere-standard-site-feed"; 11 10 ··· 39 38 container.addClass("standard-site-view"); 40 39 this.renderHeader(container); 41 40 41 + const loading = container.createEl("p", { text: "Loading subscriptions..." }); 42 + const list = container.createEl("div", { cls: "standard-site-list" }); 42 43 43 - const loading = container.createEl("p", { text: "Loading feed..." }); 44 44 try { 45 - const pubs = await getSubscribedPublications(this.plugin.client, this.plugin.settings.identifier); 46 - loading.remove(); 47 - 48 - if (pubs.length === 0) { 45 + const subsResp = await getSubscriptions(this.plugin.client, this.plugin.settings.identifier); 46 + if (subsResp.records.length === 0) { 47 + loading.remove(); 49 48 container.createEl("p", { text: "No subscriptions found" }); 50 49 return; 51 50 } 52 51 53 - const list = container.createEl("div", { cls: "standard-site-list" }); 54 - 55 - for (const pub of pubs) { 56 - void this.renderPublicationCard(list, pub); 52 + const pubUris = subsResp.records.map(sub => sub.value.publication); 53 + for (const uri of pubUris) { 54 + try { 55 + const pub = await getPublication(this.plugin.client, uri); 56 + void this.renderPublicationCard(list, pub); 57 + } catch (e) { 58 + console.warn(`Failed to fetch publication at ${uri}:`, e); 59 + } 57 60 } 61 + 62 + loading.remove(); 58 63 } catch (error) { 59 64 const message = error instanceof Error ? error.message : String(error); 60 65 console.error("Failed to load feed:", error); 61 66 container.createEl("p", { text: `Failed to load feed: ${message}`, cls: "standard-site-error" }); 62 - } finally { 63 67 loading.remove(); 64 68 } 65 69 }