diff --git a/src/api/accounts.ts b/src/api/accounts.ts index 520d524..3b422f4 100644 --- a/src/api/accounts.ts +++ b/src/api/accounts.ts @@ -1,7 +1,9 @@ import { type AppController } from '@/app.ts'; +import { nip19 } from '@/deps.ts'; import { getAuthor } from '../client.ts'; import { toAccount } from '../transmute.ts'; +import { bech32ToPubkey, isNostrId } from '../utils.ts'; const credentialsController: AppController = async (c) => { const pubkey = c.get('pubkey')!; @@ -14,4 +16,42 @@ const credentialsController: AppController = async (c) => { return c.json({ error: 'Could not find user.' }, 404); }; -export { credentialsController }; +const accountController: AppController = async (c) => { + const pubkey = c.req.param('id'); + + if (!isNostrId(pubkey)) { + return c.json({ error: 'Invalid account ID.' }, 422); + } + + const event = await getAuthor(pubkey); + if (event) { + return c.json(toAccount(event)); + } + + return c.json({ error: 'Could not find user.' }, 404); +}; + +const accountLookupController: AppController = async (c) => { + const acct = c.req.query('acct'); + + if (!acct) { + return c.json({ error: 'Missing `acct` query parameter.' }, 422); + } + + if (acct.includes('@')) { + // TODO: NIP-05 handling + return c.json({ error: 'NIP-05 lookups not yet implemented.' }, 422); + } + + const pubkey = bech32ToPubkey(acct); + if (pubkey) { + const event = await getAuthor(pubkey); + if (event) { + return c.json(toAccount(event)); + } + } + + return c.json({ error: 'Could not find user.' }, 404); +}; + +export { accountController, accountLookupController, credentialsController }; diff --git a/src/app.ts b/src/app.ts index 0491b3d..cdb1c85 100644 --- a/src/app.ts +++ b/src/app.ts @@ -1,6 +1,6 @@ import { type Context, cors, type Handler, Hono, type HonoEnv, type MiddlewareHandler } from '@/deps.ts'; -import { credentialsController } from './api/accounts.ts'; +import { accountController, accountLookupController, credentialsController } from './api/accounts.ts'; import { appCredentialsController, createAppController } from './api/apps.ts'; import { emptyArrayController, emptyObjectController } from './api/fallback.ts'; import homeController from './api/home.ts'; @@ -33,6 +33,8 @@ app.post('/oauth/token', createTokenController); app.post('/oauth/revoke', emptyObjectController); app.get('/api/v1/accounts/verify_credentials', requireAuth, credentialsController); +app.get('/api/v1/accounts/lookup', accountLookupController); +app.get('/api/v1/accounts/:id', accountController); app.post('/api/v1/statuses', requireAuth, createStatusController); diff --git a/src/utils.ts b/src/utils.ts index 00a7ab2..2b15583 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,4 +1,4 @@ -import { Context, getPublicKey } from '@/deps.ts'; +import { Context, getPublicKey, nip19, nip21 } from '@/deps.ts'; import { type Event } from '@/event.ts'; /** Get the current time in Nostr format. */ @@ -21,4 +21,30 @@ function getKeys(c: Context) { } } -export { eventDateComparator, getKeys, nostrNow }; +/** Return true if the value is a bech32 string, eg for use with NIP-19. */ +function isBech32(value: unknown): value is string { + return typeof value === 'string' && nip21.BECH32_REGEX.test(value); +} + +/** Return true if the value is a Nostr pubkey, private key, or event ID. */ +function isNostrId(value: unknown): value is string { + return typeof value === 'string' && /^[0-9a-f]{64}$/.test(value); +} + +/** Get pubkey from bech32 string, if applicable. */ +function bech32ToPubkey(bech32: string): string | undefined { + try { + const decoded = nip19.decode(bech32); + + switch (decoded.type) { + case 'nprofile': + return decoded.data.pubkey; + case 'npub': + return decoded.data; + } + } catch (_) { + // + } +} + +export { bech32ToPubkey, eventDateComparator, getKeys, isBech32, isNostrId, nostrNow };