diff --git a/app/api/inngest/client.ts b/app/api/inngest/client.ts index 3866ba9f..77a2266a 100644 --- a/app/api/inngest/client.ts +++ b/app/api/inngest/client.ts @@ -21,6 +21,11 @@ export type Events = { }; }; "appview/come-online": { data: {} }; + "user/migrate-to-standard": { + data: { + did: string; + }; + }; }; // Create a client to send and receive events diff --git a/app/api/inngest/functions/index_post_mention.ts b/app/api/inngest/functions/index_post_mention.ts index 9f28784e..98ebef6d 100644 --- a/app/api/inngest/functions/index_post_mention.ts +++ b/app/api/inngest/functions/index_post_mention.ts @@ -55,10 +55,12 @@ export const index_post_mention = inngest.createFunction( authorDid = did; } else { // Publication post: look up by custom domain + // Support both old format (pub.leaflet.publication with base_path) and + // new format (site.standard.publication with url as https://domain) let { data: pub, error } = await supabaseServerClient .from("publications") .select("*") - .eq("record->>base_path", url.host) + .or(`record->>base_path.eq.${url.host},record->>url.eq.https://${url.host}`) .single(); if (!pub) { diff --git a/app/api/inngest/functions/migrate_user_to_standard.ts b/app/api/inngest/functions/migrate_user_to_standard.ts new file mode 100644 index 00000000..73a7c9ec --- /dev/null +++ b/app/api/inngest/functions/migrate_user_to_standard.ts @@ -0,0 +1,427 @@ +import { supabaseServerClient } from "supabase/serverClient"; +import { inngest } from "../client"; +import { restoreOAuthSession } from "src/atproto-oauth"; +import { AtpBaseClient, SiteStandardPublication, SiteStandardDocument, SiteStandardGraphSubscription } from "lexicons/api"; +import { AtUri } from "@atproto/syntax"; +import { Json } from "supabase/database.types"; +import { normalizePublicationRecord, normalizeDocumentRecord } from "src/utils/normalizeRecords"; + +type MigrationResult = + | { success: true; oldUri: string; newUri: string; skipped?: boolean } + | { success: false; error: string }; + +async function createAuthenticatedAgent(did: string): Promise { + const result = await restoreOAuthSession(did); + if (!result.ok) { + throw new Error(`Failed to restore OAuth session: ${result.error.message}`); + } + const credentialSession = result.value; + return new AtpBaseClient( + credentialSession.fetchHandler.bind(credentialSession) + ); +} + +export const migrate_user_to_standard = inngest.createFunction( + { id: "migrate_user_to_standard" }, + { event: "user/migrate-to-standard" }, + async ({ event, step }) => { + const { did } = event.data; + + const stats = { + publicationsMigrated: 0, + documentsMigrated: 0, + userSubscriptionsMigrated: 0, + referencesUpdated: 0, + errors: [] as string[], + }; + + // Step 1: Verify OAuth session is valid + await step.run("verify-oauth-session", async () => { + const result = await restoreOAuthSession(did); + if (!result.ok) { + throw new Error(`Failed to restore OAuth session: ${result.error.message}`); + } + return { success: true }; + }); + + // Step 2: Get user's pub.leaflet.publication records + const oldPublications = await step.run("fetch-old-publications", async () => { + const { data, error } = await supabaseServerClient + .from("publications") + .select("*") + .eq("identity_did", did) + .like("uri", `at://${did}/pub.leaflet.publication/%`); + + if (error) throw new Error(`Failed to fetch publications: ${error.message}`); + return data || []; + }); + + // Step 3: Migrate each publication + const publicationUriMap: Record = {}; // old URI -> new URI + + for (const pub of oldPublications) { + const aturi = new AtUri(pub.uri); + + // Skip if already a site.standard.publication + if (aturi.collection === "site.standard.publication") { + publicationUriMap[pub.uri] = pub.uri; + continue; + } + + const rkey = aturi.rkey; + const normalized = normalizePublicationRecord(pub.record); + + if (!normalized) { + stats.errors.push(`Publication ${pub.uri}: Failed to normalize publication record`); + continue; + } + + // Build site.standard.publication record + const newRecord: SiteStandardPublication.Record = { + $type: "site.standard.publication", + name: normalized.name, + url: normalized.url, + description: normalized.description, + icon: normalized.icon, + theme: normalized.theme, + basicTheme: normalized.basicTheme, + preferences: normalized.preferences, + }; + + // Step: Write to PDS + const pdsResult = await step.run(`pds-write-publication-${pub.uri}`, async () => { + const agent = await createAuthenticatedAgent(did); + const putResult = await agent.com.atproto.repo.putRecord({ + repo: did, + collection: "site.standard.publication", + rkey, + record: newRecord, + validate: false, + }); + return { newUri: putResult.data.uri }; + }); + + const newUri = pdsResult.newUri; + + // Step: Write to database + const dbResult = await step.run(`db-write-publication-${pub.uri}`, async () => { + const { error: dbError } = await supabaseServerClient + .from("publications") + .upsert({ + uri: newUri, + identity_did: did, + name: normalized.name, + record: newRecord as Json, + }); + + if (dbError) { + return { success: false as const, error: dbError.message }; + } + return { success: true as const }; + }); + + if (dbResult.success) { + publicationUriMap[pub.uri] = newUri; + stats.publicationsMigrated++; + } else { + stats.errors.push(`Publication ${pub.uri}: Database error: ${dbResult.error}`); + } + } + + // Step 4: Get and migrate documents for these publications + const oldDocuments = await step.run("fetch-old-documents", async () => { + const oldPubUris = Object.keys(publicationUriMap); + if (oldPubUris.length === 0) return []; + + const { data, error } = await supabaseServerClient + .from("documents_in_publications") + .select("document, publication, documents(uri, data)") + .in("publication", oldPubUris); + + if (error) throw new Error(`Failed to fetch documents: ${error.message}`); + return data || []; + }); + + const documentUriMap: Record = {}; // old URI -> new URI + + for (const docRow of oldDocuments) { + if (!docRow.documents) continue; + const doc = docRow.documents as { uri: string; data: Json }; + const aturi = new AtUri(doc.uri); + + // Skip if already a site.standard.document + if (aturi.collection === "site.standard.document") { + documentUriMap[doc.uri] = doc.uri; + continue; + } + + const rkey = aturi.rkey; + const normalized = normalizeDocumentRecord(doc.data); + + if (!normalized) { + stats.errors.push(`Document ${doc.uri}: Failed to normalize document record`); + continue; + } + + // Get the new publication URI + const newPubUri = publicationUriMap[docRow.publication]; + if (!newPubUri) { + stats.errors.push(`Document ${doc.uri}: No migrated publication found`); + continue; + } + + // Build site.standard.document record + const newRecord: SiteStandardDocument.Record = { + $type: "site.standard.document", + title: normalized.title || "Untitled", + site: newPubUri, + publishedAt: normalized.publishedAt || new Date().toISOString(), + description: normalized.description, + content: normalized.content, + path: normalized.path, + tags: normalized.tags, + coverImage: normalized.coverImage, + bskyPostRef: normalized.bskyPostRef, + }; + + // Step: Write to PDS + const pdsResult = await step.run(`pds-write-document-${doc.uri}`, async () => { + const agent = await createAuthenticatedAgent(did); + const putResult = await agent.com.atproto.repo.putRecord({ + repo: did, + collection: "site.standard.document", + rkey, + record: newRecord, + validate: false, + }); + return { newUri: putResult.data.uri }; + }); + + const newUri = pdsResult.newUri; + + // Step: Write to database + const dbResult = await step.run(`db-write-document-${doc.uri}`, async () => { + const { error: dbError } = await supabaseServerClient + .from("documents") + .upsert({ + uri: newUri, + data: newRecord as Json, + }); + + if (dbError) { + return { success: false as const, error: dbError.message }; + } + + // Add to documents_in_publications with new URIs + await supabaseServerClient + .from("documents_in_publications") + .upsert({ + publication: newPubUri, + document: newUri, + }); + + return { success: true as const }; + }); + + if (dbResult.success) { + documentUriMap[doc.uri] = newUri; + stats.documentsMigrated++; + } else { + stats.errors.push(`Document ${doc.uri}: Database error: ${dbResult.error}`); + } + } + + // Step 5: Update references in database tables + await step.run("update-references", async () => { + // Update leaflets_in_publications - update publication and doc references + for (const [oldUri, newUri] of Object.entries(publicationUriMap)) { + const { error } = await supabaseServerClient + .from("leaflets_in_publications") + .update({ publication: newUri }) + .eq("publication", oldUri); + + if (!error) stats.referencesUpdated++; + } + + for (const [oldUri, newUri] of Object.entries(documentUriMap)) { + const { error } = await supabaseServerClient + .from("leaflets_in_publications") + .update({ doc: newUri }) + .eq("doc", oldUri); + + if (!error) stats.referencesUpdated++; + } + + // Update leaflets_to_documents - update document references + for (const [oldUri, newUri] of Object.entries(documentUriMap)) { + const { error } = await supabaseServerClient + .from("leaflets_to_documents") + .update({ document: newUri }) + .eq("document", oldUri); + + if (!error) stats.referencesUpdated++; + } + + // Update publication_domains - update publication references + for (const [oldUri, newUri] of Object.entries(publicationUriMap)) { + const { error } = await supabaseServerClient + .from("publication_domains") + .update({ publication: newUri }) + .eq("publication", oldUri); + + if (!error) stats.referencesUpdated++; + } + + // Update comments_on_documents - update document references + for (const [oldUri, newUri] of Object.entries(documentUriMap)) { + const { error } = await supabaseServerClient + .from("comments_on_documents") + .update({ document: newUri }) + .eq("document", oldUri); + + if (!error) stats.referencesUpdated++; + } + + // Update document_mentions_in_bsky - update document references + for (const [oldUri, newUri] of Object.entries(documentUriMap)) { + const { error } = await supabaseServerClient + .from("document_mentions_in_bsky") + .update({ document: newUri }) + .eq("document", oldUri); + + if (!error) stats.referencesUpdated++; + } + + // Update subscribers_to_publications - update publication references + for (const [oldUri, newUri] of Object.entries(publicationUriMap)) { + const { error } = await supabaseServerClient + .from("subscribers_to_publications") + .update({ publication: newUri }) + .eq("publication", oldUri); + + if (!error) stats.referencesUpdated++; + } + + // Update publication_subscriptions - update publication references for incoming subscriptions + for (const [oldUri, newUri] of Object.entries(publicationUriMap)) { + const { error } = await supabaseServerClient + .from("publication_subscriptions") + .update({ publication: newUri }) + .eq("publication", oldUri); + + if (!error) stats.referencesUpdated++; + } + + return stats.referencesUpdated; + }); + + // Step 6: Migrate user's own subscriptions - subscriptions BY this user to other publications + const userSubscriptions = await step.run("fetch-user-subscriptions", async () => { + const { data, error } = await supabaseServerClient + .from("publication_subscriptions") + .select("*") + .eq("identity", did) + .like("uri", `at://${did}/pub.leaflet.graph.subscription/%`); + + if (error) throw new Error(`Failed to fetch user subscriptions: ${error.message}`); + return data || []; + }); + + const userSubscriptionUriMap: Record = {}; // old URI -> new URI + + for (const sub of userSubscriptions) { + const aturi = new AtUri(sub.uri); + + // Skip if already a site.standard.graph.subscription + if (aturi.collection === "site.standard.graph.subscription") { + userSubscriptionUriMap[sub.uri] = sub.uri; + continue; + } + + const rkey = aturi.rkey; + + // Build site.standard.graph.subscription record + const newRecord: SiteStandardGraphSubscription.Record = { + $type: "site.standard.graph.subscription", + publication: sub.publication, + }; + + // Step: Write to PDS + const pdsResult = await step.run(`pds-write-subscription-${sub.uri}`, async () => { + const agent = await createAuthenticatedAgent(did); + const putResult = await agent.com.atproto.repo.putRecord({ + repo: did, + collection: "site.standard.graph.subscription", + rkey, + record: newRecord, + validate: false, + }); + return { newUri: putResult.data.uri }; + }); + + const newUri = pdsResult.newUri; + + // Step: Write to database + const dbResult = await step.run(`db-write-subscription-${sub.uri}`, async () => { + const { error: dbError } = await supabaseServerClient + .from("publication_subscriptions") + .update({ + uri: newUri, + record: newRecord as Json, + }) + .eq("uri", sub.uri); + + if (dbError) { + return { success: false as const, error: dbError.message }; + } + return { success: true as const }; + }); + + if (dbResult.success) { + userSubscriptionUriMap[sub.uri] = newUri; + stats.userSubscriptionsMigrated++; + } else { + stats.errors.push(`User subscription ${sub.uri}: Database error: ${dbResult.error}`); + } + } + + // Step 7: Delete old records from our database tables + await step.run("delete-old-db-records", async () => { + const oldPubUris = Object.keys(publicationUriMap).filter(uri => + new AtUri(uri).collection === "pub.leaflet.publication" + ); + const oldDocUris = Object.keys(documentUriMap).filter(uri => + new AtUri(uri).collection === "pub.leaflet.document" + ); + + // NOTE: We intentionally keep old documents_in_publications entries. + // New entries are created in Step 4 with the new URIs, but the old entries + // should remain so that notifications and other references that point to + // old document/publication URIs can still look up the relationship. + + // Delete from documents (old document URIs) + if (oldDocUris.length > 0) { + await supabaseServerClient + .from("documents") + .delete() + .in("uri", oldDocUris); + } + + // Delete from publications (old publication URIs) + if (oldPubUris.length > 0) { + await supabaseServerClient + .from("publications") + .delete() + .in("uri", oldPubUris); + } + }); + + return { + success: stats.errors.length === 0, + stats, + publicationUriMap, + documentUriMap, + userSubscriptionUriMap, + }; + } +); diff --git a/app/api/inngest/route.tsx b/app/api/inngest/route.tsx index 0f2375cd..f8886099 100644 --- a/app/api/inngest/route.tsx +++ b/app/api/inngest/route.tsx @@ -4,6 +4,7 @@ import { index_post_mention } from "./functions/index_post_mention"; import { come_online } from "./functions/come_online"; import { batched_update_profiles } from "./functions/batched_update_profiles"; import { index_follows } from "./functions/index_follows"; +import { migrate_user_to_standard } from "./functions/migrate_user_to_standard"; export const { GET, POST, PUT } = serve({ client: inngest, @@ -12,5 +13,6 @@ export const { GET, POST, PUT } = serve({ come_online, batched_update_profiles, index_follows, + migrate_user_to_standard, ], }); diff --git a/app/api/rpc/[command]/get_publication_data.ts b/app/api/rpc/[command]/get_publication_data.ts index 6a58f951..a9bee7ad 100644 --- a/app/api/rpc/[command]/get_publication_data.ts +++ b/app/api/rpc/[command]/get_publication_data.ts @@ -4,6 +4,7 @@ import type { Env } from "./route"; import { AtUri } from "@atproto/syntax"; import { getFactsFromHomeLeaflets } from "./getFactsFromHomeLeaflets"; import { normalizeDocumentRecord } from "src/utils/normalizeRecords"; +import { ids } from "lexicons/api/lexicons"; export type GetPublicationDataReturnType = Awaited< ReturnType<(typeof get_publication_data)["handler"]> @@ -18,11 +19,17 @@ export const get_publication_data = makeRoute({ { did, publication_name }, { supabase }: Pick, ) => { - let uri; + let pubLeafletUri; + let siteStandardUri; if (/^(?!\.$|\.\.S)[A-Za-z0-9._:~-]{1,512}$/.test(publication_name)) { - uri = AtUri.make( + pubLeafletUri = AtUri.make( did, - "pub.leaflet.publication", + ids.PubLeafletPublication, + publication_name, + ).toString(); + siteStandardUri = AtUri.make( + did, + ids.SiteStandardPublication, publication_name, ).toString(); } @@ -45,7 +52,7 @@ export const get_publication_data = makeRoute({ ) )`, ) - .or(`name.eq."${publication_name}", uri.eq."${uri}"`) + .or(`name.eq."${publication_name}", uri.eq."${pubLeafletUri}", uri.eq."${siteStandardUri}"`) .eq("identity_did", did) .single(); diff --git a/app/lish/createPub/updatePublication.ts b/app/lish/createPub/updatePublication.ts index 20fbab54..9d7e01a7 100644 --- a/app/lish/createPub/updatePublication.ts +++ b/app/lish/createPub/updatePublication.ts @@ -4,6 +4,7 @@ import { AtpBaseClient, PubLeafletPublication, PubLeafletThemeColor, + SiteStandardPublication, } from "lexicons/api"; import { restoreOAuthSession, OAuthSessionError } from "src/atproto-oauth"; import { getIdentityData } from "actions/getIdentityData"; @@ -66,20 +67,15 @@ export async function updatePublication({ // Preserve existing schema when updating const publicationType = getPublicationType(aturi.collection); - let record = { - $type: publicationType, - ...(existingPub.record as object), - name, - } as PubLeafletPublication.Record; - if (preferences) { - record.preferences = preferences; - } - - if (description !== undefined) { - record.description = description; - } + // Normalize the existing record to read its properties + const normalizedPub = normalizePublicationRecord(existingPub.record); + // Extract base_path from url if it exists (url format is https://domain, base_path is just domain) + const existingBasePath = normalizedPub?.url + ? normalizedPub.url.replace(/^https?:\/\//, "") + : undefined; - // Upload the icon if provided How do I tell if there isn't a new one? + // Upload the icon if provided + let iconBlob = normalizedPub?.icon; if (iconFile && iconFile.size > 0) { const buffer = await iconFile.arrayBuffer(); const uploadResult = await agent.com.atproto.repo.uploadBlob( @@ -88,10 +84,51 @@ export async function updatePublication({ ); if (uploadResult.data.blob) { - record.icon = uploadResult.data.blob; + iconBlob = uploadResult.data.blob; } } + // Build preferences based on input or existing normalized preferences + const preferencesData = preferences || normalizedPub?.preferences; + + // Build the record with the correct field based on publication type + const record = + publicationType === "site.standard.publication" + ? ({ + $type: publicationType, + name, + description: description !== undefined ? description : normalizedPub?.description, + icon: iconBlob, + theme: normalizedPub?.theme, + preferences: preferencesData + ? { + $type: "site.standard.publication#preferences" as const, + showInDiscover: preferencesData.showInDiscover, + showComments: preferencesData.showComments, + showMentions: preferencesData.showMentions, + showPrevNext: preferencesData.showPrevNext, + } + : undefined, + url: normalizedPub?.url || "", + } as SiteStandardPublication.Record) + : ({ + $type: publicationType, + name, + description: description !== undefined ? description : normalizedPub?.description, + icon: iconBlob, + theme: normalizedPub?.theme, + preferences: preferencesData + ? { + $type: "pub.leaflet.publication#preferences" as const, + showInDiscover: preferencesData.showInDiscover, + showComments: preferencesData.showComments, + showMentions: preferencesData.showMentions, + showPrevNext: preferencesData.showPrevNext, + } + : undefined, + base_path: existingBasePath, + } as PubLeafletPublication.Record); + let result = await agent.com.atproto.repo.putRecord({ repo: credentialSession.did!, rkey: aturi.rkey, @@ -159,23 +196,43 @@ export async function updatePublicationBasePath({ ? normalizedPub.url.replace(/^https?:\/\//, "") : undefined; - let record = { - $type: publicationType, - name: normalizedPub?.name || "", - description: normalizedPub?.description, - icon: normalizedPub?.icon, - theme: normalizedPub?.theme, - preferences: normalizedPub?.preferences - ? { - $type: "pub.leaflet.publication#preferences" as const, - showInDiscover: normalizedPub.preferences.showInDiscover, - showComments: normalizedPub.preferences.showComments, - showMentions: normalizedPub.preferences.showMentions, - showPrevNext: normalizedPub.preferences.showPrevNext, - } - : undefined, - base_path, - } as PubLeafletPublication.Record; + // Build the record with the correct field based on publication type + const record = + publicationType === "site.standard.publication" + ? ({ + $type: publicationType, + name: normalizedPub?.name || "", + description: normalizedPub?.description, + icon: normalizedPub?.icon, + theme: normalizedPub?.theme, + preferences: normalizedPub?.preferences + ? { + $type: "site.standard.publication#preferences" as const, + showInDiscover: normalizedPub.preferences.showInDiscover, + showComments: normalizedPub.preferences.showComments, + showMentions: normalizedPub.preferences.showMentions, + showPrevNext: normalizedPub.preferences.showPrevNext, + } + : undefined, + url: `https://${base_path}`, + } as SiteStandardPublication.Record) + : ({ + $type: publicationType, + name: normalizedPub?.name || "", + description: normalizedPub?.description, + icon: normalizedPub?.icon, + theme: normalizedPub?.theme, + preferences: normalizedPub?.preferences + ? { + $type: "pub.leaflet.publication#preferences" as const, + showInDiscover: normalizedPub.preferences.showInDiscover, + showComments: normalizedPub.preferences.showComments, + showMentions: normalizedPub.preferences.showMentions, + showPrevNext: normalizedPub.preferences.showPrevNext, + } + : undefined, + base_path, + } as PubLeafletPublication.Record); let result = await agent.com.atproto.repo.putRecord({ repo: credentialSession.did!, @@ -257,58 +314,81 @@ export async function updatePublicationTheme({ ? normalizedPub.url.replace(/^https?:\/\//, "") : undefined; - let record = { - $type: publicationType, - name: normalizedPub?.name || "", - description: normalizedPub?.description, - icon: normalizedPub?.icon, - base_path: existingBasePath, - preferences: normalizedPub?.preferences + // Build theme object (shared between both publication types) + const themeData = { + backgroundImage: theme.backgroundImage + ? { + $type: "pub.leaflet.theme.backgroundImage", + image: ( + await agent.com.atproto.repo.uploadBlob( + new Uint8Array(await theme.backgroundImage.arrayBuffer()), + { encoding: theme.backgroundImage.type }, + ) + )?.data.blob, + width: theme.backgroundRepeat || undefined, + repeat: !!theme.backgroundRepeat, + } + : theme.backgroundImage === null + ? undefined + : normalizedPub?.theme?.backgroundImage, + backgroundColor: theme.backgroundColor ? { - $type: "pub.leaflet.publication#preferences" as const, - showInDiscover: normalizedPub.preferences.showInDiscover, - showComments: normalizedPub.preferences.showComments, - showMentions: normalizedPub.preferences.showMentions, - showPrevNext: normalizedPub.preferences.showPrevNext, + ...theme.backgroundColor, } : undefined, - theme: { - backgroundImage: theme.backgroundImage - ? { - $type: "pub.leaflet.theme.backgroundImage", - image: ( - await agent.com.atproto.repo.uploadBlob( - new Uint8Array(await theme.backgroundImage.arrayBuffer()), - { encoding: theme.backgroundImage.type }, - ) - )?.data.blob, - width: theme.backgroundRepeat || undefined, - repeat: !!theme.backgroundRepeat, - } - : theme.backgroundImage === null - ? undefined - : normalizedPub?.theme?.backgroundImage, - backgroundColor: theme.backgroundColor - ? { - ...theme.backgroundColor, - } - : undefined, - pageWidth: theme.pageWidth, - primary: { - ...theme.primary, - }, - pageBackground: { - ...theme.pageBackground, - }, - showPageBackground: theme.showPageBackground, - accentBackground: { - ...theme.accentBackground, - }, - accentText: { - ...theme.accentText, - }, + pageWidth: theme.pageWidth, + primary: { + ...theme.primary, + }, + pageBackground: { + ...theme.pageBackground, + }, + showPageBackground: theme.showPageBackground, + accentBackground: { + ...theme.accentBackground, }, - } as PubLeafletPublication.Record; + accentText: { + ...theme.accentText, + }, + }; + + // Build the record with the correct field based on publication type + const record = + publicationType === "site.standard.publication" + ? ({ + $type: publicationType, + name: normalizedPub?.name || "", + description: normalizedPub?.description, + icon: normalizedPub?.icon, + url: normalizedPub?.url || "", + preferences: normalizedPub?.preferences + ? { + $type: "site.standard.publication#preferences" as const, + showInDiscover: normalizedPub.preferences.showInDiscover, + showComments: normalizedPub.preferences.showComments, + showMentions: normalizedPub.preferences.showMentions, + showPrevNext: normalizedPub.preferences.showPrevNext, + } + : undefined, + theme: themeData, + } as SiteStandardPublication.Record) + : ({ + $type: publicationType, + name: normalizedPub?.name || "", + description: normalizedPub?.description, + icon: normalizedPub?.icon, + base_path: existingBasePath, + preferences: normalizedPub?.preferences + ? { + $type: "pub.leaflet.publication#preferences" as const, + showInDiscover: normalizedPub.preferences.showInDiscover, + showComments: normalizedPub.preferences.showComments, + showMentions: normalizedPub.preferences.showMentions, + showPrevNext: normalizedPub.preferences.showPrevNext, + } + : undefined, + theme: themeData, + } as PubLeafletPublication.Record); let result = await agent.com.atproto.repo.putRecord({ repo: credentialSession.did!, diff --git a/app/lish/subscribeToPublication.ts b/app/lish/subscribeToPublication.ts index e18d30fe..e321361d 100644 --- a/app/lish/subscribeToPublication.ts +++ b/app/lish/subscribeToPublication.ts @@ -48,7 +48,7 @@ export async function subscribeToPublication( let agent = new AtpBaseClient( credentialSession.fetchHandler.bind(credentialSession), ); - let record = await agent.pub.leaflet.graph.subscription.create( + let record = await agent.site.standard.graph.subscription.create( { repo: credentialSession.did!, rkey: TID.nextStr() }, { publication, @@ -140,10 +140,14 @@ export async function unsubscribeToPublication( .eq("publication", publication) .single(); if (!existingSubscription) return { success: true }; - await agent.pub.leaflet.graph.subscription.delete({ - repo: credentialSession.did!, - rkey: new AtUri(existingSubscription.uri).rkey, - }); + + // Delete from both collections (old and new schema) - one or both may exist + let rkey = new AtUri(existingSubscription.uri).rkey; + await Promise.all([ + agent.pub.leaflet.graph.subscription.delete({ repo: credentialSession.did!, rkey }).catch(() => {}), + agent.site.standard.graph.subscription.delete({ repo: credentialSession.did!, rkey }).catch(() => {}), + ]); + await supabaseServerClient .from("publication_subscriptions") .delete() diff --git a/appview/index.ts b/appview/index.ts index c229e0e1..b9f5a2de 100644 --- a/appview/index.ts +++ b/appview/index.ts @@ -247,7 +247,10 @@ async function handleEvent(evt: Event) { if (docResult.error) console.log(docResult.error); // site.standard.document uses "site" field to reference the publication - if (record.value.site) { + // For documents in publications, site is an AT-URI (at://did:plc:xxx/site.standard.publication/rkey) + // For standalone documents, site is an HTTPS URL (https://leaflet.pub/p/did:plc:xxx) + // Only link to publications table for AT-URI sites + if (record.value.site && record.value.site.startsWith("at://")) { let siteURI = new AtUri(record.value.site); if (siteURI.host !== evt.uri.host) {