From 2acfecc1eb2b558bb30db9f0288b93845b254fa4 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sat, 4 Mar 2023 23:26:25 -0600 Subject: [PATCH] Allow posting a status to Nostr --- src/api/accounts.ts | 10 ++++------ src/api/statuses.ts | 36 ++++++++++++++++++++++++++++++++++++ src/app.ts | 3 +++ src/client.ts | 2 +- src/config.ts | 1 + src/deps.ts | 2 +- src/publisher.ts | 20 ++++++++++++++++++++ src/transmute.ts | 39 +++++++++++++++++++++++++++++++++++++++ src/utils.ts | 17 +++++++++++++++++ 9 files changed, 122 insertions(+), 8 deletions(-) create mode 100644 src/api/statuses.ts create mode 100644 src/publisher.ts create mode 100644 src/transmute.ts create mode 100644 src/utils.ts diff --git a/src/api/accounts.ts b/src/api/accounts.ts index 120f7fd..2ece6cb 100644 --- a/src/api/accounts.ts +++ b/src/api/accounts.ts @@ -1,17 +1,15 @@ -import { getPublicKey } from '@/deps.ts'; - import { LOCAL_DOMAIN } from '../config.ts'; import { fetchUser } from '../client.ts'; import { MetaContent, metaContentSchema } from '../schema.ts'; +import { getKeys } from '../utils.ts'; import type { Context } from '@/deps.ts'; async function credentialsController(c: Context) { - const authHeader = c.req.headers.get('Authorization') || ''; + const keys = getKeys(c); - if (authHeader.startsWith('Bearer ')) { - const token = authHeader.split('Bearer ')[1]; - const pubkey = getPublicKey(token); + if (keys) { + const { pubkey } = keys; const event = await fetchUser(pubkey); const parsed = metaContentSchema.safeParse(JSON.parse(event?.content || '')); const content: MetaContent = parsed.success ? parsed.data : {}; diff --git a/src/api/statuses.ts b/src/api/statuses.ts new file mode 100644 index 0000000..6f5dc7a --- /dev/null +++ b/src/api/statuses.ts @@ -0,0 +1,36 @@ +import { validator, z } from '@/deps.ts'; + +import publish from '../publisher.ts'; +import { toStatus } from '../transmute.ts'; +import { getKeys } from '../utils.ts'; + +import type { Event } from '../event.ts'; + +const createStatusSchema = z.object({ + status: z.string(), +}); + +const createStatusController = validator('json', (value, c) => { + const keys = getKeys(c); + const result = createStatusSchema.safeParse(value); + + if (result.success && keys) { + const { data } = result; + const { pubkey, privatekey } = keys; + + const event: Event<1> = { + kind: 1, + pubkey: pubkey, + content: data.status, + tags: [], + created_at: Math.floor(new Date().getTime() / 1000), + }; + + publish(event, privatekey); + return c.json(toStatus(event)); + } else { + return c.json({ error: 'Bad request' }, 400); + } +}); + +export { createStatusController }; diff --git a/src/app.ts b/src/app.ts index 95470fe..63ce8ac 100644 --- a/src/app.ts +++ b/src/app.ts @@ -5,6 +5,7 @@ import { appCredentialsController, createAppController } from './api/apps.ts'; import { emptyArrayController } from './api/fallback.ts'; import instanceController from './api/instance.ts'; import { createTokenController } from './api/oauth.ts'; +import { createStatusController } from './api/statuses.ts'; const app = new Hono(); @@ -19,6 +20,8 @@ app.post('/oauth/token', createTokenController); app.get('/api/v1/accounts/verify_credentials', credentialsController); +app.post('/api/v1/statuses', createStatusController); + // Not (yet) implemented. app.get('/api/v1/timelines/*', emptyArrayController); app.get('/api/v1/accounts/:id/statuses', emptyArrayController); diff --git a/src/client.ts b/src/client.ts index a9ae376..19268e3 100644 --- a/src/client.ts +++ b/src/client.ts @@ -34,4 +34,4 @@ const fetchFollows = (pubkey: string): Promise | null> => { }); }; -export { fetchEvent, fetchFollows, fetchUser }; +export { fetchEvent, fetchFollows, fetchUser, pool }; diff --git a/src/config.ts b/src/config.ts index 66c9fab..42e5096 100644 --- a/src/config.ts +++ b/src/config.ts @@ -2,3 +2,4 @@ export const LOCAL_DOMAIN = Deno.env.get('LOCAL_DOMAIN') || 'http://localhost:80 export const POST_CHAR_LIMIT = Number(Deno.env.get('POST_CHAR_LIMIT') || 5000); export const poolRelays = (Deno.env.get('RELAY_POOL') || '').split(',').filter(Boolean); +export const publishRelays = ['wss://relay.mostr.pub']; diff --git a/src/deps.ts b/src/deps.ts index 104db22..23acd59 100644 --- a/src/deps.ts +++ b/src/deps.ts @@ -3,5 +3,5 @@ export { Hono, validator }; export { cors } from 'https://deno.land/x/hono@v3.0.2/middleware.ts'; export { z } from 'https://deno.land/x/zod@v3.20.5/mod.ts'; export { Author, RelayPool } from 'https://dev.jspm.io/nostr-relaypool@0.5.3'; -export { getPublicKey } from 'https://dev.jspm.io/nostr-tools@1.6.0'; +export { getEventHash, getPublicKey, signEvent } from 'https://dev.jspm.io/nostr-tools@1.6.0'; export type { Context }; diff --git a/src/publisher.ts b/src/publisher.ts new file mode 100644 index 0000000..c2cebc3 --- /dev/null +++ b/src/publisher.ts @@ -0,0 +1,20 @@ +import { getEventHash, signEvent } from '@/deps.ts'; + +import { pool } from './client.ts'; +import { publishRelays } from './config.ts'; + +import type { Event } from './event.ts'; + +/** Publish an event to the Nostr relay. */ +function publish(event: Event, privatekey: string, relays = publishRelays): void { + event.id = getEventHash(event); + event.sig = signEvent(event, privatekey); + console.log('Publishing event', event); + try { + pool.publish(event, relays); + } catch (e) { + console.error(e); + } +} + +export default publish; diff --git a/src/transmute.ts b/src/transmute.ts new file mode 100644 index 0000000..972c043 --- /dev/null +++ b/src/transmute.ts @@ -0,0 +1,39 @@ +import { LOCAL_DOMAIN } from './config.ts'; + +import type { Event } from './event.ts'; + +function toStatus(event: Event<1>) { + return { + id: event.id, + account: { + id: event.pubkey, + }, + content: event.content, + created_at: new Date(event.created_at * 1000).toISOString(), + in_reply_to_id: null, + in_reply_to_account_id: null, + sensitive: false, + spoiler_text: '', + visibility: 'public', + language: 'en', + replies_count: 0, + reblogs_count: 0, + favourites_count: 0, + favourited: false, + reblogged: false, + muted: false, + bookmarked: false, + reblog: null, + application: null, + media_attachments: [], + mentions: [], + tags: [], + emojis: [], + card: null, + poll: null, + uri: `${LOCAL_DOMAIN}/posts/${event.id}`, + url: `${LOCAL_DOMAIN}/posts/${event.id}`, + }; +} + +export { toStatus }; diff --git a/src/utils.ts b/src/utils.ts new file mode 100644 index 0000000..498ee71 --- /dev/null +++ b/src/utils.ts @@ -0,0 +1,17 @@ +import { Context, getPublicKey } from '@/deps.ts'; + +function getKeys(c: Context) { + const auth = c.req.headers.get('Authorization') || ''; + + if (auth.startsWith('Bearer ')) { + const privatekey = auth.split('Bearer ')[1]; + const pubkey = getPublicKey(privatekey); + + return { + privatekey, + pubkey, + }; + } +} + +export { getKeys };