Allow posting a status to Nostr

This commit is contained in:
Alex Gleason 2023-03-04 23:26:25 -06:00
parent 73884c212f
commit 2acfecc1eb
No known key found for this signature in database
GPG Key ID: 7211D1F99744FBB7
9 changed files with 122 additions and 8 deletions

View File

@ -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 : {};

36
src/api/statuses.ts Normal file
View File

@ -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 };

View File

@ -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);

View File

@ -34,4 +34,4 @@ const fetchFollows = (pubkey: string): Promise<Event<3> | null> => {
});
};
export { fetchEvent, fetchFollows, fetchUser };
export { fetchEvent, fetchFollows, fetchUser, pool };

View File

@ -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'];

View File

@ -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 };

20
src/publisher.ts Normal file
View File

@ -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;

39
src/transmute.ts Normal file
View File

@ -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 };

17
src/utils.ts Normal file
View File

@ -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 };