From c39fd2daa2b5178e0d32356ef95a903792e355a5 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sat, 15 Jun 2024 23:41:19 -0500 Subject: [PATCH 1/2] Improve the setup script and clean up config --- scripts/setup.ts | 73 ++++++++++++++++++++++------------ src/config.ts | 68 +++++++++++++++---------------- src/views/mastodon/statuses.ts | 4 +- 3 files changed, 83 insertions(+), 62 deletions(-) diff --git a/scripts/setup.ts b/scripts/setup.ts index 255d8aa0..4223d318 100644 --- a/scripts/setup.ts +++ b/scripts/setup.ts @@ -1,22 +1,44 @@ +import nodeUrl from 'node:url'; import { exists } from '@std/fs/exists'; import { generateSecretKey, nip19 } from 'nostr-tools'; import question from 'question-deno'; -const vars: Record = {}; +import { Conf } from '@/config.ts'; + +console.log(''); +console.log('Hello! Welcome to the Ditto setup tool. We will ask you a few questions to generate a .env file for you.'); +console.log(''); +console.log('- Ditto docs: https://docs.soapbox.pub/ditto/'); if (await exists('./.env')) { - const overwrite = await question('confirm', 'Overwrite existing .env file? (this is a destructive action)', false); - if (!overwrite) { - console.log('Aborted'); - Deno.exit(0); - } + console.log('- Your existing .env file will be overwritten.'); } -console.log('Generating secret key...'); -const sk = generateSecretKey(); -vars.DITTO_NSEC = nip19.nsecEncode(sk); +console.log('- Press Ctrl+D to exit at any time.'); +console.log(''); -const domain = await question('input', 'What is the domain of your instance? (eg ditto.pub)'); +const vars: Record = {}; + +const DITTO_NSEC = Deno.env.get('DITTO_NSEC'); + +if (DITTO_NSEC) { + const choice = await question('list', 'Looks like you already have a DITTO_NSEC. Should we keep it?', [ + 'keep', + 'create new (destructive)', + ]); + if (choice === 'keep') { + vars.DITTO_NSEC = DITTO_NSEC; + } + if (choice === 'create new (destructive)') { + vars.DITTO_NSEC = nip19.nsecEncode(generateSecretKey()); + console.log(' Generated secret key\n'); + } +} else { + vars.DITTO_NSEC = nip19.nsecEncode(generateSecretKey()); + console.log(' Generated secret key\n'); +} + +const domain = await question('input', 'What is the domain of your instance? (eg ditto.pub)', Conf.url.host); vars.LOCAL_DOMAIN = `https://${domain}`; const database = await question('list', 'Which database do you want to use?', ['postgres', 'sqlite']); @@ -25,11 +47,12 @@ if (database === 'sqlite') { vars.DATABASE_URL = `sqlite://${path}`; } if (database === 'postgres') { - const host = await question('input', 'Postgres host', 'localhost'); - const port = await question('input', 'Postgres port', '5432'); - const user = await question('input', 'Postgres user', 'ditto'); - const password = await question('input', 'Postgres password', 'ditto'); - const database = await question('input', 'Postgres database', 'ditto'); + const url = nodeUrl.parse(Deno.env.get('DATABASE_URL') ?? 'postgres://ditto:ditto@localhost:5432/ditto'); + const host = await question('input', 'Postgres host', url.hostname); + const port = await question('input', 'Postgres port', url.port); + const user = await question('input', 'Postgres user', url.username); + const password = await question('input', 'Postgres password', url.password); + const database = await question('input', 'Postgres database', url.pathname.slice(1)); vars.DATABASE_URL = `postgres://${user}:${password}@${host}:${port}/${database}`; } @@ -42,28 +65,28 @@ vars.DITTO_UPLOADER = await question('list', 'How do you want to upload files?', ]); if (vars.DITTO_UPLOADER === 'nostrbuild') { - vars.NOSTRBUILD_ENDPOINT = await question('input', 'nostr.build endpoint', 'https://nostr.build/api/v2/upload/files'); + vars.NOSTRBUILD_ENDPOINT = await question('input', 'nostr.build endpoint', Conf.nostrbuildEndpoint); } if (vars.DITTO_UPLOADER === 'blossom') { - vars.BLOSSOM_SERVERS = await question('input', 'Blossom servers (comma separated)', 'https://blossom.primal.net/'); + vars.BLOSSOM_SERVERS = await question('input', 'Blossom servers (comma separated)', Conf.blossomServers.join(',')); } if (vars.DITTO_UPLOADER === 's3') { - vars.S3_ACCESS_KEY = await question('input', 'S3 access key'); - vars.S3_SECRET_KEY = await question('input', 'S3 secret key'); - vars.S3_ENDPOINT = await question('input', 'S3 endpoint'); - vars.S3_BUCKET = await question('input', 'S3 bucket'); - vars.S3_REGION = await question('input', 'S3 region'); - vars.S3_PATH_STYLE = String(await question('confirm', 'Use path style?', false)); + vars.S3_ACCESS_KEY = await question('input', 'S3 access key', Conf.s3.accessKey); + vars.S3_SECRET_KEY = await question('input', 'S3 secret key', Conf.s3.secretKey); + vars.S3_ENDPOINT = await question('input', 'S3 endpoint', Conf.s3.endPoint); + vars.S3_BUCKET = await question('input', 'S3 bucket', Conf.s3.bucket); + vars.S3_REGION = await question('input', 'S3 region', Conf.s3.region); + vars.S3_PATH_STYLE = String(await question('confirm', 'Use path style?', Conf.s3.pathStyle ?? false)); const mediaDomain = await question('input', 'Media domain', `media.${domain}`); vars.MEDIA_DOMAIN = `https://${mediaDomain}`; } if (vars.DITTO_UPLOADER === 'ipfs') { - vars.IPFS_API_URL = await question('input', 'IPFS API URL', 'http://localhost:5001'); + vars.IPFS_API_URL = await question('input', 'IPFS API URL', Conf.ipfs.apiUrl); const mediaDomain = await question('input', 'Media domain', `media.${domain}`); vars.MEDIA_DOMAIN = `https://${mediaDomain}`; } if (vars.DITTO_UPLOADER === 'local') { - vars.UPLOADS_DIR = await question('input', 'Local uploads directory', 'data/uploads'); + vars.UPLOADS_DIR = await question('input', 'Local uploads directory', Conf.uploadsDir); const mediaDomain = await question('input', 'Media domain', `media.${domain}`); vars.MEDIA_DOMAIN = `https://${mediaDomain}`; } diff --git a/src/config.ts b/src/config.ts index e1c0f103..6dc765ff 100644 --- a/src/config.ts +++ b/src/config.ts @@ -13,8 +13,9 @@ await dotenv.load({ /** Application-wide configuration. */ class Conf { + private static _pubkey: string | undefined; /** Ditto admin secret key in nip19 format. This is the way it's configured by an admin. */ - static get nsec() { + static get nsec(): `nsec1${string}` { const value = Deno.env.get('DITTO_NSEC'); if (!value) { throw new Error('Missing DITTO_NSEC'); @@ -25,13 +26,18 @@ class Conf { return value as `nsec1${string}`; } /** Ditto admin secret key in hex format. */ - static get seckey() { + static get seckey(): Uint8Array { return nip19.decode(Conf.nsec).data; } /** Ditto admin public key in hex format. */ - static pubkey = getPublicKey(Conf.seckey); + static get pubkey(): string { + if (!this._pubkey) { + this._pubkey = getPublicKey(Conf.seckey); + } + return this._pubkey; + } /** Ditto admin secret key as a Web Crypto key. */ - static get cryptoKey() { + static get cryptoKey(): Promise { return crypto.subtle.importKey( 'raw', Conf.seckey, @@ -41,7 +47,7 @@ class Conf { ); } - static get port() { + static get port(): number { return parseInt(Deno.env.get('PORT') || '4036'); } @@ -50,17 +56,13 @@ class Conf { return `${protocol === 'https:' ? 'wss:' : 'ws:'}//${host}/relay`; } /** Relay to use for NIP-50 `search` queries. */ - static get searchRelay() { + static get searchRelay(): string | undefined { return Deno.env.get('SEARCH_RELAY'); } /** Origin of the Ditto server, including the protocol and port. */ - static get localDomain() { + static get localDomain(): string { return Deno.env.get('LOCAL_DOMAIN') || `http://localhost:${Conf.port}`; } - /** URL to an external Nostr viewer. */ - static get externalDomain() { - return Deno.env.get('NOSTR_EXTERNAL') || Conf.localDomain; - } /** * Heroku-style database URL. This is used in production to connect to the * database. @@ -76,7 +78,7 @@ class Conf { } static db = { get url(): url.UrlWithStringQuery { - return url.parse(Deno.env.get('DATABASE_URL') ?? 'sqlite://data/db.sqlite3'); + return url.parse(Conf.databaseUrl); }, get dialect(): 'sqlite' | 'postgres' | undefined { switch (Conf.db.url.protocol) { @@ -90,43 +92,43 @@ class Conf { }, }; /** Character limit to enforce for posts made through Mastodon API. */ - static get postCharLimit() { + static get postCharLimit(): number { return Number(Deno.env.get('POST_CHAR_LIMIT') || 5000); } /** S3 media storage configuration. */ static s3 = { - get endPoint() { + get endPoint(): string | undefined { return Deno.env.get('S3_ENDPOINT')!; }, - get region() { + get region(): string | undefined { return Deno.env.get('S3_REGION')!; }, - get accessKey() { + get accessKey(): string | undefined { return Deno.env.get('S3_ACCESS_KEY'); }, - get secretKey() { + get secretKey(): string | undefined { return Deno.env.get('S3_SECRET_KEY'); }, - get bucket() { + get bucket(): string | undefined { return Deno.env.get('S3_BUCKET'); }, - get pathStyle() { + get pathStyle(): boolean | undefined { return optionalBooleanSchema.parse(Deno.env.get('S3_PATH_STYLE')); }, - get port() { + get port(): number | undefined { return optionalNumberSchema.parse(Deno.env.get('S3_PORT')); }, - get sessionToken() { + get sessionToken(): string | undefined { return Deno.env.get('S3_SESSION_TOKEN'); }, - get useSSL() { + get useSSL(): boolean | undefined { return optionalBooleanSchema.parse(Deno.env.get('S3_USE_SSL')); }, }; /** IPFS uploader configuration. */ static ipfs = { /** Base URL for private IPFS API calls. */ - get apiUrl() { + get apiUrl(): string { return Deno.env.get('IPFS_API_URL') || 'http://localhost:5001'; }, }; @@ -139,15 +141,15 @@ class Conf { return Deno.env.get('BLOSSOM_SERVERS')?.split(',') || ['https://blossom.primal.net/']; } /** Module to upload files with. */ - static get uploader() { + static get uploader(): string | undefined { return Deno.env.get('DITTO_UPLOADER'); } /** Location to use for local uploads. */ - static get uploadsDir() { + static get uploadsDir(): string | undefined { return Deno.env.get('UPLOADS_DIR') || 'data/uploads'; } /** Media base URL for uploads. */ - static get mediaDomain() { + static get mediaDomain(): string { const value = Deno.env.get('MEDIA_DOMAIN'); if (!value) { @@ -159,11 +161,11 @@ class Conf { return value; } /** Max upload size for files in number of bytes. Default 100MiB. */ - static get maxUploadSize() { + static get maxUploadSize(): number { return Number(Deno.env.get('MAX_UPLOAD_SIZE') || 100 * 1024 * 1024); } /** Usernames that regular users cannot sign up with. */ - static get forbiddenUsernames() { + static get forbiddenUsernames(): string[] { return Deno.env.get('FORBIDDEN_USERNAMES')?.split(',') || [ '_', 'admin', @@ -175,24 +177,20 @@ class Conf { } /** Proof-of-work configuration. */ static pow = { - get registrations() { + get registrations(): number { return Number(Deno.env.get('DITTO_POW_REGISTRATIONS') ?? 20); }, }; /** Domain of the Ditto server as a `URL` object, for easily grabbing the `hostname`, etc. */ - static get url() { + static get url(): URL { return new URL(Conf.localDomain); } /** Merges the path with the localDomain. */ static local(path: string): string { return mergePaths(Conf.localDomain, path); } - /** Get an external URL for the NIP-19 identifier. */ - static external(nip19: string): string { - return new URL(`/${nip19}`, Conf.externalDomain).toString(); - } /** URL to send Sentry errors to. */ - static get sentryDsn() { + static get sentryDsn(): string | undefined { return Deno.env.get('SENTRY_DSN'); } /** SQLite settings. */ diff --git a/src/views/mastodon/statuses.ts b/src/views/mastodon/statuses.ts index 4408c607..d440b65c 100644 --- a/src/views/mastodon/statuses.ts +++ b/src/views/mastodon/statuses.ts @@ -121,8 +121,8 @@ async function renderStatus(event: DittoEvent, opts: RenderStatusOpts): Promise< poll: null, quote: !event.quote ? null : await renderStatus(event.quote, { depth: depth + 1 }), quote_id: event.quote?.id ?? null, - uri: Conf.external(note), - url: Conf.external(note), + uri: Conf.local(`/${note}`), + url: Conf.local(`/${note}`), zapped: Boolean(zapEvent), pleroma: { emoji_reactions: reactions, From c3af8299f1b7f9b8dd5e947e5a1046bb6249ac1e Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sat, 15 Jun 2024 23:51:50 -0500 Subject: [PATCH 2/2] Spread s3 config Fixes https://gitlab.com/soapbox-pub/ditto/-/issues/156 --- src/config.ts | 6 +++--- src/middleware/uploaderMiddleware.ts | 15 ++++++++++++++- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/src/config.ts b/src/config.ts index 6dc765ff..502544d8 100644 --- a/src/config.ts +++ b/src/config.ts @@ -98,10 +98,10 @@ class Conf { /** S3 media storage configuration. */ static s3 = { get endPoint(): string | undefined { - return Deno.env.get('S3_ENDPOINT')!; + return Deno.env.get('S3_ENDPOINT'); }, get region(): string | undefined { - return Deno.env.get('S3_REGION')!; + return Deno.env.get('S3_REGION'); }, get accessKey(): string | undefined { return Deno.env.get('S3_ACCESS_KEY'); @@ -145,7 +145,7 @@ class Conf { return Deno.env.get('DITTO_UPLOADER'); } /** Location to use for local uploads. */ - static get uploadsDir(): string | undefined { + static get uploadsDir(): string { return Deno.env.get('UPLOADS_DIR') || 'data/uploads'; } /** Media base URL for uploads. */ diff --git a/src/middleware/uploaderMiddleware.ts b/src/middleware/uploaderMiddleware.ts index 38e8aceb..96a47336 100644 --- a/src/middleware/uploaderMiddleware.ts +++ b/src/middleware/uploaderMiddleware.ts @@ -13,7 +13,20 @@ export const uploaderMiddleware: AppMiddleware = async (c, next) => { switch (Conf.uploader) { case 's3': - c.set('uploader', new S3Uploader(Conf.s3)); + c.set( + 'uploader', + new S3Uploader({ + accessKey: Conf.s3.accessKey, + bucket: Conf.s3.bucket, + endPoint: Conf.s3.endPoint!, + pathStyle: Conf.s3.pathStyle, + port: Conf.s3.port, + region: Conf.s3.region!, + secretKey: Conf.s3.secretKey, + sessionToken: Conf.s3.sessionToken, + useSSL: Conf.s3.useSSL, + }), + ); break; case 'ipfs': c.set('uploader', new IPFSUploader({ baseUrl: Conf.mediaDomain, apiUrl: Conf.ipfs.apiUrl, fetch: fetchWorker }));