diff --git a/package.json b/package.json index 03cf079d..196420c3 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ }, "devDependencies": { "@atproto/api": "^0.13.18", + "@atproto/did": "^0.1.3", "@codemirror/lang-html": "^6.4.9", "@codemirror/lang-markdown": "^6.3.1", "@codemirror/language": "^6.10.4", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2eedb725..ff3668f1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -23,6 +23,9 @@ importers: '@atproto/api': specifier: ^0.13.18 version: 0.13.18 + '@atproto/did': + specifier: ^0.1.3 + version: 0.1.3 '@codemirror/lang-html': specifier: ^6.4.9 version: 6.4.9 @@ -308,6 +311,9 @@ packages: '@atproto/common-web@0.3.1': resolution: {integrity: sha512-N7wiTnus5vAr+lT//0y8m/FaHHLJ9LpGuEwkwDAeV3LCiPif4m/FS8x/QOYrx1PdZQwKso95RAPzCGWQBH5j6Q==} + '@atproto/did@0.1.3': + resolution: {integrity: sha512-ULD8Gw/KRRwLFZ2Z2L4DjmdOMrg8IYYlcjdSc+GQ2/QJSVnD2zaJJVTLd3vls121wGt/583rNaiZTT2DpBze4w==} + '@atproto/lexicon@0.4.3': resolution: {integrity: sha512-lFVZXe1S1pJP0dcxvJuHP3r/a+EAIBwwU7jUK+r8iLhIja+ml6NmYv8KeFHmIJATh03spEQ9s02duDmFVdCoXg==} @@ -3346,6 +3352,10 @@ snapshots: uint8arrays: 3.0.0 zod: 3.23.8 + '@atproto/did@0.1.3': + dependencies: + zod: 3.23.8 + '@atproto/lexicon@0.4.3': dependencies: '@atproto/common-web': 0.3.1 diff --git a/src/lib/dns/server.ts b/src/lib/dns/server.ts index ef0510e8..06d76251 100644 --- a/src/lib/dns/server.ts +++ b/src/lib/dns/server.ts @@ -16,6 +16,8 @@ import { RCode } from 'dinodns/common/core/utils'; import { redis } from '$lib/redis'; import { serverGlobals } from '$lib/server-globals'; import { usernames } from '$lib/usernames'; +import { getAtProtoDid, profileLinkByUsername } from '$lib/leaf/profile'; +import { isDid } from '@atproto/did'; const REDIS_USER_PREFIX = 'weird:users:names:'; const REDIS_DNS_RECORD_PREFIX = 'weird:dns:records:'; @@ -311,27 +313,46 @@ export async function startDnsServer() { const suffix = usernames.publicSuffix(name); switch (type) { case 'TXT': - if (!name.startsWith('_weird.')) return returnAnswers(null); if (!suffix) return returnAnswers(null); - const txtUsername = name.split('_weird.')[1]; - if (!txtUsername) return returnAnswers(null); - const pubkey = await redis.hGet(REDIS_USER_PREFIX + txtUsername, 'subspace'); - if (!pubkey) return returnAnswers(null); - returnAnswers([ - // TODO: Resolve Iroh ticket / NodeID along with subspace - // { - // name, - // type, - // data: `server=${await leafClient.node_id()}`, - // ttl: DNS_TTL - // }, - { - name, - type, - data: `subspace=${pubkey}`, - ttl: DNS_TTL - } - ]); + if (name.startsWith('_weird')) { + const txtUsername = name.split('_weird.')[1]; + if (!txtUsername) return returnAnswers(null); + const pubkey = await redis.hGet(REDIS_USER_PREFIX + txtUsername, 'subspace'); + if (!pubkey) return returnAnswers(null); + returnAnswers([ + // TODO: Resolve Iroh ticket / NodeID along with subspace + // { + // name, + // type, + // data: `server=${await leafClient.node_id()}`, + // ttl: DNS_TTL + // }, + { + name, + type, + data: `subspace=${pubkey}`, + ttl: DNS_TTL + } + ]); + } else if (name.startsWith('_atproto')) { + const txtUsername = name.split('atproto.')[1]; + if (!txtUsername) return returnAnswers(null); + const profileLink = await profileLinkByUsername(txtUsername); + if (!profileLink) return returnAnswers(null); + const atprotoDid = await getAtProtoDid(profileLink); + if (!atprotoDid) return returnAnswers(null); + if (!isDid(atprotoDid)) return returnAnswers(null); + returnAnswers([ + { + name, + type, + data: `did=${atprotoDid}`, + ttl: DNS_TTL + } + ]); + } else { + return returnAnswers(null); + } break; case 'A': if (!suffix) return returnAnswers(null); diff --git a/src/lib/leaf/profile.ts b/src/lib/leaf/profile.ts index 6b9af516..c6c3a3c2 100644 --- a/src/lib/leaf/profile.ts +++ b/src/lib/leaf/profile.ts @@ -22,7 +22,6 @@ export interface Profile { username: string; server: string; }; - pubpage_theme?: string; } export class Tags extends Component { @@ -191,22 +190,20 @@ export class MastodonProfile extends Component { } } -export class WeirdPubpageTheme extends Component { +export class AtprotoDid extends Component { value: string = ''; constructor(s: string) { super(); this.value = s; } static componentName(): string { - return 'WeirdPubpageTheme'; + return 'AtProtoDid'; } static borshSchema(): BorshSchema { return BorshSchema.String; } static specification(): Component[] { - return [ - new CommonMark(`The name of the theme selected by a user for their main Weird pubpage.`) - ]; + return [new CommonMark(`The DID associated to the user's AtProto / Bluesky account.`)]; } } @@ -261,9 +258,9 @@ export async function getProfile(link: ExactLink): Promise Tags, WeirdCustomDomain, MastodonProfile, - WeirdPubpageTheme, WebLinks, - SocialLinks + SocialLinks, + AtprotoDid ); return ( (ent && { @@ -273,8 +270,7 @@ export async function getProfile(link: ExactLink): Promise // custom_domain: ent.get(WeirdCustomDomain)?.value, social_links: ent.get(SocialLinks)?.value || [], links: ent.get(WebLinks)?.value || [], - mastodon_profile: ent.get(MastodonProfile)?.value, - pubpage_theme: ent.get(WeirdPubpageTheme)?.value + mastodon_profile: ent.get(MastodonProfile)?.value }) || undefined ); @@ -288,13 +284,20 @@ export async function setRawProfile(link: ExactLink, profile: Profile) { profile.display_name ? new Name(profile.display_name) : Name, profile.bio ? new Description(profile.bio) : Description, profile.mastodon_profile ? new MastodonProfile(profile.mastodon_profile) : MastodonProfile, - profile.pubpage_theme ? new WeirdPubpageTheme(profile.pubpage_theme) : WeirdPubpageTheme, profile.links ? new WebLinks(profile.links) : WebLinks, profile.social_links ? new SocialLinks(profile.social_links) : SocialLinks, profile.tags ? new Tags(profile.tags) : Tags ]); } +export async function setAtprotoDid(link: ExactLink, did?: string): Promise { + await leafClient.update_components(link, [did ? new AtprotoDid(did) : AtprotoDid]); +} + +export async function getAtProtoDid(link: ExactLink): Promise { + return (await leafClient.get_components(link, AtprotoDid))?.get(AtprotoDid)?.value; +} + export async function setCustomDomain(userId: string, domain?: string): Promise { const link = await profileLinkById(userId); diff --git a/src/routes/(app)/[username]/+layout.server.ts b/src/routes/(app)/[username]/+layout.server.ts index 66383425..b423c406 100644 --- a/src/routes/(app)/[username]/+layout.server.ts +++ b/src/routes/(app)/[username]/+layout.server.ts @@ -1,4 +1,4 @@ -import { getProfile, listChildren } from '$lib/leaf/profile'; +import { getAtProtoDid, getProfile, listChildren } from '$lib/leaf/profile'; import { error, redirect } from '@sveltejs/kit'; import type { LayoutServerLoad } from '../$types'; import { getSession, getUserInfoFromSession } from '$lib/rauthy/server'; @@ -23,7 +23,8 @@ export const load: LayoutServerLoad = async ({ fetch, params, request, url }) => const subspace = await usernames.getSubspace(fullUsername); if (!subspace) return error(404, `User not found: ${fullUsername}`); - const profile = (await getProfile(subspace_link(subspace, null))) || { + const profileLink = subspace_link(subspace, null); + const profile = (await getProfile(profileLink)) || { tags: [], links: [], social_links: [] @@ -64,8 +65,11 @@ export const load: LayoutServerLoad = async ({ fetch, params, request, url }) => ? await getUserInfoFromSession(fetch, request, sessionInfo) : undefined; + const atprotoDid = await getAtProtoDid(profileLink); + return { userInfo, + atprotoDid, providers, profile, verifiedLinks: await verifiedLinks.get(fullUsername), diff --git a/src/routes/(app)/[username]/+layout.svelte b/src/routes/(app)/[username]/+layout.svelte index 448f54e7..850ff8b0 100644 --- a/src/routes/(app)/[username]/+layout.svelte +++ b/src/routes/(app)/[username]/+layout.svelte @@ -45,6 +45,7 @@ component: { ref: ManageAccountModal }, userInfo: data.userInfo, providers: data.providers, + atprotoDid: data.atprotoDid, async response(r) { if ('error' in r) { error = r.error; diff --git a/src/routes/(app)/[username]/+page.server.ts b/src/routes/(app)/[username]/+page.server.ts index b6b7579a..22de3a4a 100644 --- a/src/routes/(app)/[username]/+page.server.ts +++ b/src/routes/(app)/[username]/+page.server.ts @@ -48,6 +48,7 @@ export const actions = { link.url = 'https://' + link.url; } } + let bio = data.get('bio')?.toString() || undefined; if (bio === '') { bio = undefined; @@ -68,11 +69,6 @@ export const actions = { }) || undefined; - let pubpage_theme = data.get('subsite_theme')?.toString() || undefined; - if (pubpage_theme === '') { - pubpage_theme = undefined; - } - const profile: Profile = { display_name, tags, @@ -80,7 +76,6 @@ export const actions = { social_links, bio, mastodon_profile, - pubpage_theme }; try { diff --git a/src/routes/(app)/[username]/components/ManageAccountModal.svelte b/src/routes/(app)/[username]/components/ManageAccountModal.svelte index 8aa3a901..d9f3ca4f 100644 --- a/src/routes/(app)/[username]/components/ManageAccountModal.svelte +++ b/src/routes/(app)/[username]/components/ManageAccountModal.svelte @@ -1,5 +1,5 @@ {#if $modalStore[0]} -
-
-
Manage Account
+ {/if} + + diff --git a/src/routes/(app)/[username]/settings/setAtprotoDid/+server.ts b/src/routes/(app)/[username]/settings/setAtprotoDid/+server.ts new file mode 100644 index 00000000..3f447e2d --- /dev/null +++ b/src/routes/(app)/[username]/settings/setAtprotoDid/+server.ts @@ -0,0 +1,18 @@ +import { getSession } from '$lib/rauthy/server'; +import { error, type RequestHandler } from '@sveltejs/kit'; +import { profileLinkById, setAtprotoDid } from '$lib/leaf/profile'; +import { isDid } from '@atproto/did'; + +export const POST: RequestHandler = async ({ request, fetch }) => { + const did = await request.text(); + if (!isDid(did)) return error(400, 'Invalid DID'); + + const { sessionInfo } = await getSession(fetch, request); + if (!sessionInfo) { + return new Response(null, { status: 403 }); + } + const profileLink = await profileLinkById(sessionInfo.user_id); + await setAtprotoDid(profileLink, did || undefined); + + return new Response(); +};