mirror of
https://gitlab.com/soapbox-pub/ditto.git
synced 2025-12-06 11:29:46 +00:00
Improve the setup script and clean up config
This commit is contained in:
parent
287c89b9fd
commit
c39fd2daa2
3 changed files with 83 additions and 62 deletions
|
|
@ -1,22 +1,44 @@
|
||||||
|
import nodeUrl from 'node:url';
|
||||||
import { exists } from '@std/fs/exists';
|
import { exists } from '@std/fs/exists';
|
||||||
import { generateSecretKey, nip19 } from 'nostr-tools';
|
import { generateSecretKey, nip19 } from 'nostr-tools';
|
||||||
import question from 'question-deno';
|
import question from 'question-deno';
|
||||||
|
|
||||||
const vars: Record<string, string | undefined> = {};
|
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')) {
|
if (await exists('./.env')) {
|
||||||
const overwrite = await question('confirm', 'Overwrite existing .env file? (this is a destructive action)', false);
|
console.log('- Your existing .env file will be overwritten.');
|
||||||
if (!overwrite) {
|
|
||||||
console.log('Aborted');
|
|
||||||
Deno.exit(0);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('Generating secret key...');
|
console.log('- Press Ctrl+D to exit at any time.');
|
||||||
const sk = generateSecretKey();
|
console.log('');
|
||||||
vars.DITTO_NSEC = nip19.nsecEncode(sk);
|
|
||||||
|
|
||||||
const domain = await question('input', 'What is the domain of your instance? (eg ditto.pub)');
|
const vars: Record<string, string | undefined> = {};
|
||||||
|
|
||||||
|
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}`;
|
vars.LOCAL_DOMAIN = `https://${domain}`;
|
||||||
|
|
||||||
const database = await question('list', 'Which database do you want to use?', ['postgres', 'sqlite']);
|
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}`;
|
vars.DATABASE_URL = `sqlite://${path}`;
|
||||||
}
|
}
|
||||||
if (database === 'postgres') {
|
if (database === 'postgres') {
|
||||||
const host = await question('input', 'Postgres host', 'localhost');
|
const url = nodeUrl.parse(Deno.env.get('DATABASE_URL') ?? 'postgres://ditto:ditto@localhost:5432/ditto');
|
||||||
const port = await question('input', 'Postgres port', '5432');
|
const host = await question('input', 'Postgres host', url.hostname);
|
||||||
const user = await question('input', 'Postgres user', 'ditto');
|
const port = await question('input', 'Postgres port', url.port);
|
||||||
const password = await question('input', 'Postgres password', 'ditto');
|
const user = await question('input', 'Postgres user', url.username);
|
||||||
const database = await question('input', 'Postgres database', 'ditto');
|
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}`;
|
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') {
|
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') {
|
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') {
|
if (vars.DITTO_UPLOADER === 's3') {
|
||||||
vars.S3_ACCESS_KEY = await question('input', 'S3 access key');
|
vars.S3_ACCESS_KEY = await question('input', 'S3 access key', Conf.s3.accessKey);
|
||||||
vars.S3_SECRET_KEY = await question('input', 'S3 secret key');
|
vars.S3_SECRET_KEY = await question('input', 'S3 secret key', Conf.s3.secretKey);
|
||||||
vars.S3_ENDPOINT = await question('input', 'S3 endpoint');
|
vars.S3_ENDPOINT = await question('input', 'S3 endpoint', Conf.s3.endPoint);
|
||||||
vars.S3_BUCKET = await question('input', 'S3 bucket');
|
vars.S3_BUCKET = await question('input', 'S3 bucket', Conf.s3.bucket);
|
||||||
vars.S3_REGION = await question('input', 'S3 region');
|
vars.S3_REGION = await question('input', 'S3 region', Conf.s3.region);
|
||||||
vars.S3_PATH_STYLE = String(await question('confirm', 'Use path style?', false));
|
vars.S3_PATH_STYLE = String(await question('confirm', 'Use path style?', Conf.s3.pathStyle ?? false));
|
||||||
const mediaDomain = await question('input', 'Media domain', `media.${domain}`);
|
const mediaDomain = await question('input', 'Media domain', `media.${domain}`);
|
||||||
vars.MEDIA_DOMAIN = `https://${mediaDomain}`;
|
vars.MEDIA_DOMAIN = `https://${mediaDomain}`;
|
||||||
}
|
}
|
||||||
if (vars.DITTO_UPLOADER === 'ipfs') {
|
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}`);
|
const mediaDomain = await question('input', 'Media domain', `media.${domain}`);
|
||||||
vars.MEDIA_DOMAIN = `https://${mediaDomain}`;
|
vars.MEDIA_DOMAIN = `https://${mediaDomain}`;
|
||||||
}
|
}
|
||||||
if (vars.DITTO_UPLOADER === 'local') {
|
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}`);
|
const mediaDomain = await question('input', 'Media domain', `media.${domain}`);
|
||||||
vars.MEDIA_DOMAIN = `https://${mediaDomain}`;
|
vars.MEDIA_DOMAIN = `https://${mediaDomain}`;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,8 +13,9 @@ await dotenv.load({
|
||||||
|
|
||||||
/** Application-wide configuration. */
|
/** Application-wide configuration. */
|
||||||
class Conf {
|
class Conf {
|
||||||
|
private static _pubkey: string | undefined;
|
||||||
/** Ditto admin secret key in nip19 format. This is the way it's configured by an admin. */
|
/** 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');
|
const value = Deno.env.get('DITTO_NSEC');
|
||||||
if (!value) {
|
if (!value) {
|
||||||
throw new Error('Missing DITTO_NSEC');
|
throw new Error('Missing DITTO_NSEC');
|
||||||
|
|
@ -25,13 +26,18 @@ class Conf {
|
||||||
return value as `nsec1${string}`;
|
return value as `nsec1${string}`;
|
||||||
}
|
}
|
||||||
/** Ditto admin secret key in hex format. */
|
/** Ditto admin secret key in hex format. */
|
||||||
static get seckey() {
|
static get seckey(): Uint8Array {
|
||||||
return nip19.decode(Conf.nsec).data;
|
return nip19.decode(Conf.nsec).data;
|
||||||
}
|
}
|
||||||
/** Ditto admin public key in hex format. */
|
/** 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. */
|
/** Ditto admin secret key as a Web Crypto key. */
|
||||||
static get cryptoKey() {
|
static get cryptoKey(): Promise<CryptoKey> {
|
||||||
return crypto.subtle.importKey(
|
return crypto.subtle.importKey(
|
||||||
'raw',
|
'raw',
|
||||||
Conf.seckey,
|
Conf.seckey,
|
||||||
|
|
@ -41,7 +47,7 @@ class Conf {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
static get port() {
|
static get port(): number {
|
||||||
return parseInt(Deno.env.get('PORT') || '4036');
|
return parseInt(Deno.env.get('PORT') || '4036');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -50,17 +56,13 @@ class Conf {
|
||||||
return `${protocol === 'https:' ? 'wss:' : 'ws:'}//${host}/relay`;
|
return `${protocol === 'https:' ? 'wss:' : 'ws:'}//${host}/relay`;
|
||||||
}
|
}
|
||||||
/** Relay to use for NIP-50 `search` queries. */
|
/** Relay to use for NIP-50 `search` queries. */
|
||||||
static get searchRelay() {
|
static get searchRelay(): string | undefined {
|
||||||
return Deno.env.get('SEARCH_RELAY');
|
return Deno.env.get('SEARCH_RELAY');
|
||||||
}
|
}
|
||||||
/** Origin of the Ditto server, including the protocol and port. */
|
/** 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}`;
|
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
|
* Heroku-style database URL. This is used in production to connect to the
|
||||||
* database.
|
* database.
|
||||||
|
|
@ -76,7 +78,7 @@ class Conf {
|
||||||
}
|
}
|
||||||
static db = {
|
static db = {
|
||||||
get url(): url.UrlWithStringQuery {
|
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 {
|
get dialect(): 'sqlite' | 'postgres' | undefined {
|
||||||
switch (Conf.db.url.protocol) {
|
switch (Conf.db.url.protocol) {
|
||||||
|
|
@ -90,43 +92,43 @@ class Conf {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
/** Character limit to enforce for posts made through Mastodon API. */
|
/** 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);
|
return Number(Deno.env.get('POST_CHAR_LIMIT') || 5000);
|
||||||
}
|
}
|
||||||
/** S3 media storage configuration. */
|
/** S3 media storage configuration. */
|
||||||
static s3 = {
|
static s3 = {
|
||||||
get endPoint() {
|
get endPoint(): string | undefined {
|
||||||
return Deno.env.get('S3_ENDPOINT')!;
|
return Deno.env.get('S3_ENDPOINT')!;
|
||||||
},
|
},
|
||||||
get region() {
|
get region(): string | undefined {
|
||||||
return Deno.env.get('S3_REGION')!;
|
return Deno.env.get('S3_REGION')!;
|
||||||
},
|
},
|
||||||
get accessKey() {
|
get accessKey(): string | undefined {
|
||||||
return Deno.env.get('S3_ACCESS_KEY');
|
return Deno.env.get('S3_ACCESS_KEY');
|
||||||
},
|
},
|
||||||
get secretKey() {
|
get secretKey(): string | undefined {
|
||||||
return Deno.env.get('S3_SECRET_KEY');
|
return Deno.env.get('S3_SECRET_KEY');
|
||||||
},
|
},
|
||||||
get bucket() {
|
get bucket(): string | undefined {
|
||||||
return Deno.env.get('S3_BUCKET');
|
return Deno.env.get('S3_BUCKET');
|
||||||
},
|
},
|
||||||
get pathStyle() {
|
get pathStyle(): boolean | undefined {
|
||||||
return optionalBooleanSchema.parse(Deno.env.get('S3_PATH_STYLE'));
|
return optionalBooleanSchema.parse(Deno.env.get('S3_PATH_STYLE'));
|
||||||
},
|
},
|
||||||
get port() {
|
get port(): number | undefined {
|
||||||
return optionalNumberSchema.parse(Deno.env.get('S3_PORT'));
|
return optionalNumberSchema.parse(Deno.env.get('S3_PORT'));
|
||||||
},
|
},
|
||||||
get sessionToken() {
|
get sessionToken(): string | undefined {
|
||||||
return Deno.env.get('S3_SESSION_TOKEN');
|
return Deno.env.get('S3_SESSION_TOKEN');
|
||||||
},
|
},
|
||||||
get useSSL() {
|
get useSSL(): boolean | undefined {
|
||||||
return optionalBooleanSchema.parse(Deno.env.get('S3_USE_SSL'));
|
return optionalBooleanSchema.parse(Deno.env.get('S3_USE_SSL'));
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
/** IPFS uploader configuration. */
|
/** IPFS uploader configuration. */
|
||||||
static ipfs = {
|
static ipfs = {
|
||||||
/** Base URL for private IPFS API calls. */
|
/** Base URL for private IPFS API calls. */
|
||||||
get apiUrl() {
|
get apiUrl(): string {
|
||||||
return Deno.env.get('IPFS_API_URL') || 'http://localhost:5001';
|
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/'];
|
return Deno.env.get('BLOSSOM_SERVERS')?.split(',') || ['https://blossom.primal.net/'];
|
||||||
}
|
}
|
||||||
/** Module to upload files with. */
|
/** Module to upload files with. */
|
||||||
static get uploader() {
|
static get uploader(): string | undefined {
|
||||||
return Deno.env.get('DITTO_UPLOADER');
|
return Deno.env.get('DITTO_UPLOADER');
|
||||||
}
|
}
|
||||||
/** Location to use for local uploads. */
|
/** Location to use for local uploads. */
|
||||||
static get uploadsDir() {
|
static get uploadsDir(): string | undefined {
|
||||||
return Deno.env.get('UPLOADS_DIR') || 'data/uploads';
|
return Deno.env.get('UPLOADS_DIR') || 'data/uploads';
|
||||||
}
|
}
|
||||||
/** Media base URL for uploads. */
|
/** Media base URL for uploads. */
|
||||||
static get mediaDomain() {
|
static get mediaDomain(): string {
|
||||||
const value = Deno.env.get('MEDIA_DOMAIN');
|
const value = Deno.env.get('MEDIA_DOMAIN');
|
||||||
|
|
||||||
if (!value) {
|
if (!value) {
|
||||||
|
|
@ -159,11 +161,11 @@ class Conf {
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
/** Max upload size for files in number of bytes. Default 100MiB. */
|
/** 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);
|
return Number(Deno.env.get('MAX_UPLOAD_SIZE') || 100 * 1024 * 1024);
|
||||||
}
|
}
|
||||||
/** Usernames that regular users cannot sign up with. */
|
/** Usernames that regular users cannot sign up with. */
|
||||||
static get forbiddenUsernames() {
|
static get forbiddenUsernames(): string[] {
|
||||||
return Deno.env.get('FORBIDDEN_USERNAMES')?.split(',') || [
|
return Deno.env.get('FORBIDDEN_USERNAMES')?.split(',') || [
|
||||||
'_',
|
'_',
|
||||||
'admin',
|
'admin',
|
||||||
|
|
@ -175,24 +177,20 @@ class Conf {
|
||||||
}
|
}
|
||||||
/** Proof-of-work configuration. */
|
/** Proof-of-work configuration. */
|
||||||
static pow = {
|
static pow = {
|
||||||
get registrations() {
|
get registrations(): number {
|
||||||
return Number(Deno.env.get('DITTO_POW_REGISTRATIONS') ?? 20);
|
return Number(Deno.env.get('DITTO_POW_REGISTRATIONS') ?? 20);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
/** Domain of the Ditto server as a `URL` object, for easily grabbing the `hostname`, etc. */
|
/** 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);
|
return new URL(Conf.localDomain);
|
||||||
}
|
}
|
||||||
/** Merges the path with the localDomain. */
|
/** Merges the path with the localDomain. */
|
||||||
static local(path: string): string {
|
static local(path: string): string {
|
||||||
return mergePaths(Conf.localDomain, path);
|
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. */
|
/** URL to send Sentry errors to. */
|
||||||
static get sentryDsn() {
|
static get sentryDsn(): string | undefined {
|
||||||
return Deno.env.get('SENTRY_DSN');
|
return Deno.env.get('SENTRY_DSN');
|
||||||
}
|
}
|
||||||
/** SQLite settings. */
|
/** SQLite settings. */
|
||||||
|
|
|
||||||
|
|
@ -121,8 +121,8 @@ async function renderStatus(event: DittoEvent, opts: RenderStatusOpts): Promise<
|
||||||
poll: null,
|
poll: null,
|
||||||
quote: !event.quote ? null : await renderStatus(event.quote, { depth: depth + 1 }),
|
quote: !event.quote ? null : await renderStatus(event.quote, { depth: depth + 1 }),
|
||||||
quote_id: event.quote?.id ?? null,
|
quote_id: event.quote?.id ?? null,
|
||||||
uri: Conf.external(note),
|
uri: Conf.local(`/${note}`),
|
||||||
url: Conf.external(note),
|
url: Conf.local(`/${note}`),
|
||||||
zapped: Boolean(zapEvent),
|
zapped: Boolean(zapEvent),
|
||||||
pleroma: {
|
pleroma: {
|
||||||
emoji_reactions: reactions,
|
emoji_reactions: reactions,
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue