feat(utils): refactor settings to use yaml

This commit is contained in:
Anton Livaja 2023-01-11 00:59:18 -05:00 committed by Ricardo Arturo Cabral Mejía
parent 8b8c4b4eaa
commit 5a8107f73c
6 changed files with 272 additions and 379 deletions

View File

@ -354,8 +354,7 @@ You can change the default folder by setting the `NOSTR_CONFIG_DIR` environment
Run nostream using one of the quick-start guides at least once and `nostream/.nostr/settings.json` will be created. Run nostream using one of the quick-start guides at least once and `nostream/.nostr/settings.json` will be created.
Any changes made to the settings file will be read on the next start. Any changes made to the settings file will be read on the next start.
A sample settings file is included at the project root under the name `settings.sample.json`. Feel free to copy it to `nostream/.nostr/settings.json` Default settings can be found under `resources/default-settings.yaml`. Feel free to copy it to `nostream/.nostr/settings.yaml` if you would like to have a settings file before running the relay first.
if you would like to have a settings file before running the relay first.
See [CONFIGURATION.md](CONFIGURATION.md) for a detailed explanation of each environment variable and setting. See [CONFIGURATION.md](CONFIGURATION.md) for a detailed explanation of each environment variable and setting.
## Dev Channel ## Dev Channel

View File

@ -1,216 +0,0 @@
{
"info": {
"relay_url": "wss://nostream.your-domain.com",
"name": "nostream.your-domain.com",
"description": "A nostr relay written in Typescript.",
"pubkey": "replace-with-your-pubkey",
"contact": "operator@your-domain.com"
},
"payments": {
"enabled": false,
"processor": "zebedee",
"feeSchedules": {
"admission": [{
"enabled": false,
"descripton": "Admission fee charged per public key in msats (1000 msats = 1 satoshi)",
"amount": 1000000,
"whitelists": {
"pubkeys": ["replace-with-your-pubkey"]
}
}],
"publication": [
{
"enabled": false,
"description": "Publication fee charged per event in msats (1000 msats = 1 satoshi)",
"amount": 10,
"whitelists": {
"pubkeys": ["replace-with-your-pubkey"]
}
}
]
}
},
"paymentProcessors": {
"zebedee": {
"baseURL": "https://api.zebedee.io/",
"callbackBaseURL": "https://nostream.your-domain.com/callbacks/zebedee"
}
},
"network": {
"maxPayloadSize": 131072,
"remoteIpHeader": "x-forwarded-for",
"idleTimeout": 60
},
"workers": {
"count": 0
},
"limits": {
"invoice": {
"rateLimits": [
{
"period": 60000,
"rate": 1
},
{
"period": 3600000,
"rate": 30
},
{
"period": 86400000,
"rate": 360
}
],
"ipWhitelist": [
"::1",
"::ffff:10.10.10.1"
]
},
"connection": {
"rateLimits": [
{
"period": 60000,
"rate": 12
},
{
"period": 3600000,
"rate": 360
},
{
"period": 86400000,
"rate": 2880
}
],
"ipWhitelist": [
"::1",
"::ffff:10.10.10.1"
]
},
"event": {
"eventId": {
"minLeadingZeroBits": 0
},
"kind": {
"whitelist": [],
"blacklist": []
},
"pubkey": {
"minBalanceMsats": 0,
"minLeadingZeroBits": 0,
"whitelist": [],
"blacklist": []
},
"createdAt": {
"maxPositiveDelta": 900,
"maxNegativeDelta": 0
},
"content": [
{
"description": "64 KB for event kind ranges 0-10 and 40-49",
"kinds": [[0, 10], [40, 49]],
"maxLength": 65536
},
{
"description": "96 KB for event kind ranges 11-39 and 50-max",
"kinds": [[11, 39], [50, 9007199254740991]],
"maxLength": 98304
}
],
"rateLimits": [
{
"description": "6 events/min for event kinds 0, 3, 40 and 41",
"kinds": [0, 3, 40, 41],
"period": 60000,
"rate": 6
},
{
"description": "12 events/min for event kinds 1, 2, 4 and 42",
"kinds": [1, 2, 4, 42],
"period": 60000,
"rate": 12
},
{
"description": "360 events/hour for event kinds 1, 2, 4 and 42",
"kinds": [1, 2, 4, 42],
"period": 3600000,
"rate": 360
},
{
"description": "30 events/min for event kind ranges 5-7 and 43-49",
"kinds": [[5, 7], [43, 49]],
"period": 60000,
"rate": 30
},
{
"description": "24 events/min for replaceable events and parameterized replaceable events",
"kinds": [[10000, 19999], [30000, 39999]],
"period": 60000,
"rate": 24
},
{
"description": "60 events/min for ephemeral events",
"kinds": [[20000, 29999]],
"period": 60000,
"rate": 60
},
{
"description": "720 events/hour for all events",
"period": 3600000,
"rate": 720
},
{
"description": "2880 events/day for all events",
"period": 86400000,
"rate": 2880
}
],
"whitelists": {
"pubkeys": [],
"ipAddresses": [
"::1",
"::ffff:10.10.10.1"
]
}
},
"client": {
"subscription": {
"maxSubscriptions": 10,
"maxFilters": 10
}
},
"message": {
"rateLimits": [
{
"description": "60 subscriptions/min",
"types": ["REQ"],
"period": 60000,
"rate": 60
},
{
"description": "2880 subscriptions/hour",
"types": ["REQ"],
"period": 3600000,
"rate": 2880
},
{
"description": "120 raw messages/min",
"period": 60000,
"rate": 120
},
{
"description": "3600 raw messages/hour",
"period": 3600000,
"rate": 3600
},
{
"description": "86400 raw messages/day",
"period": 86400000,
"rate": 86400
}
],
"ipWhitelist": [
"::1",
"::ffff:10.10.10.1"
]
}
}
}

173
resources/default-settings.yaml Executable file
View File

@ -0,0 +1,173 @@
info:
relay_url: wss://nostream.your-domain.com
name: nostream.your-domain.com
description: A nostr relay written in Typescript.
pubkey: replace-with-your-pubkey
contact: operator@your-domain.com
payments:
enabled: false
processor: zebedee
feeSchedules:
admission:
- enabled: false
descripton: Admission fee charged per public key in msats (1000 msats = 1 satoshi)
amount: 1000000
whitelists:
pubkeys:
- replace-with-your-pubkey
publication:
- enabled: false
description: Publication fee charged per event in msats (1000 msats = 1 satoshi)
amount: 10
whitelists:
pubkeys:
- replace-with-your-pubkey
paymentProcessors:
zebedee:
baseURL: https://api.zebedee.io/
callbackBaseURL: https://nostream.your-domain.com/callbacks/zebedee
network:
maxPayloadSize: 131072
remoteIpHeader: x-forwarded-for
idleTimeout: 60
workers:
count: 0
limits:
invoice:
rateLimits:
- period: 60000
rate: 1
- period: 3600000
rate: 30
- period: 86400000
rate: 360
ipWhitelist:
- "::1"
- "::ffff:10.10.10.1"
connection:
rateLimits:
- period: 60000
rate: 12
- period: 3600000
rate: 360
- period: 86400000
rate: 2880
ipWhitelist:
- "::1"
- "::ffff:10.10.10.1"
event:
eventId:
minLeadingZeroBits: 0
kind:
whitelist: []
blacklist: []
pubkey:
minBalanceMsats: 0
minLeadingZeroBits: 0
whitelist: []
blacklist: []
createdAt:
maxPositiveDelta: 900
maxNegativeDelta: 0
content:
- description: 64 KB for event kind ranges 0-10 and 40-49
kinds:
- - 0
- 10
- - 40
- 49
maxLength: 65536
- description: 96 KB for event kind ranges 11-39 and 50-max
kinds:
- - 11
- 39
- - 50
- 9007199254740991
maxLength: 98304
rateLimits:
- description: 6 events/min for event kinds 0, 3, 40 and 41
kinds:
- 0
- 3
- 40
- 41
period: 60000
rate: 6
- description: 12 events/min for event kinds 1, 2, 4 and 42
kinds:
- 1
- 2
- 4
- 42
period: 60000
rate: 12
- description: 360 events/hour for event kinds 1, 2, 4 and 42
kinds:
- 1
- 2
- 4
- 42
period: 3600000
rate: 360
- description: 30 events/min for event kind ranges 5-7 and 43-49
kinds:
- - 5
- 7
- - 43
- 49
period: 60000
rate: 30
- description: 24 events/min for replaceable events and parameterized replaceable
events
kinds:
- - 10000
- 19999
- - 30000
- 39999
period: 60000
rate: 24
- description: 60 events/min for ephemeral events
kinds:
- - 20000
- 29999
period: 60000
rate: 60
- description: 720 events/hour for all events
period: 3600000
rate: 720
- description: 2880 events/day for all events
period: 86400000
rate: 2880
whitelists:
pubkeys: []
ipAddresses:
- "::1"
- "::ffff:10.10.10.1"
client:
subscription:
maxSubscriptions: 10
maxFilters: 10
message:
rateLimits:
- description: 60 subscriptions/min
types:
- REQ
period: 60000
rate: 60
- description: 2880 subscriptions/hour
types:
- REQ
period: 3600000
rate: 2880
- description: 120 raw messages/min
period: 60000
rate: 120
- description: 3600 raw messages/hour
period: 3600000
rate: 3600
- description: 86400 raw messages/day
period: 86400000
rate: 86400
ipWhitelist:
- "::1"
- "::ffff:10.10.10.1"

View File

@ -1,134 +0,0 @@
{
"info": {
"relay_url": "wss://nostream.your-domain.com",
"name": "nostream.your-domain.com",
"description": "A nostr relay written in TypeScript.",
"pubkey": "replace-with-your-pubkey",
"contact": "operator@your-domain.com"
},
"network": {
"maxPayloadSize": 131072,
"remoteIpHeader": "x-forwarded-for",
"idleTimeout": 60
},
"workers": {
"count": 0
},
"limits": {
"connection": {
"rateLimits": [
{
"period": 60000,
"rate": 12
},
{
"period": 3600000,
"rate": 360
},
{
"period": 86400000,
"rate": 2880
}
],
"ipWhitelist": [
"::1",
"::ffff:10.10.10.1"
]
},
"event": {
"eventId": {
"minLeadingZeroBits": 0
},
"kind": {
"whitelist": [],
"blacklist": []
},
"pubkey": {
"minBalanceMsats": 10000,
"minLeadingZeroBits": 0,
"whitelist": [],
"blacklist": []
},
"createdAt": {
"maxPositiveDelta": 900,
"maxNegativeDelta": 0
},
"content": {
"maxLength": 1048576
},
"rateLimits": [
{
"kinds": [0, 3, 40, 41],
"period": 60000,
"rate": 6
},
{
"kinds": [1, 2, 4, 42],
"period": 60000,
"rate": 12
},
{
"kinds": [1, 2, 4, 42],
"period": 3600000,
"rate": 360
},
{
"kinds": [[5, 7], [43, 49]],
"period": 60000,
"rate": 30
},
{
"kinds": [[10000, 19999], [30000, 39999]],
"period": 60000,
"rate": 24
},
{
"kinds": [[20000, 29999]],
"period": 60000,
"rate": 60
},
{
"period": 3600000,
"rate": 720
},
{
"period": 86400000,
"rate": 2880
}
],
"whitelists": {
"pubkeys": [],
"ipAddresses": [
"::1",
"::ffff:10.10.10.1"
]
}
},
"client": {
"subscription": {
"maxSubscriptions": 10,
"maxFilters": 10
}
},
"message": {
"rateLimits": [
{
"period": 60000,
"rate": 120
},
{
"period": 3600000,
"rate": 3600
},
{
"period": 86400000,
"rate": 86400
}
],
"ipWhitelist": [
"::1",
"::ffff:10.10.10.1"
]
}
}
}

View File

@ -2,25 +2,36 @@ import fs from 'fs'
import { homedir } from 'os' import { homedir } from 'os'
import { join } from 'path' import { join } from 'path'
import { mergeDeepRight } from 'ramda' import { mergeDeepRight } from 'ramda'
import yaml from 'js-yaml'
import { createLogger } from '../factories/logger-factory' import { createLogger } from '../factories/logger-factory'
import defaultSettingsJson from '../../resources/default-settings.json'
import { ISettings } from '../@types/settings' import { ISettings } from '../@types/settings'
const debug = createLogger('settings') const debug = createLogger('settings')
const FileType = {
yaml: 'yaml',
json: 'json',
}
export class SettingsStatic { export class SettingsStatic {
static _settings: ISettings static _settings: ISettings
public static getSettingsFilePath(filename = 'settings.json') { public static getSettingsFileBasePath(): string {
return join( return process.env.NOSTR_CONFIG_DIR ?? join(homedir(), '.nostr')
process.env.NOSTR_CONFIG_DIR ?? join(homedir(), '.nostr'),
filename
)
} }
public static loadSettings(path: string) { public static getDefaultSettingsFilePath(): string {
debug('loading settings from %s', path) return `${join(process.cwd(), 'resources')}/default-settings.yaml`
}
public static loadAndParseYamlFile(path: string): ISettings {
const defaultSettingsFileContent = fs.readFileSync(path, 'utf8')
const defaults = yaml.load(defaultSettingsFileContent) as ISettings
return defaults
}
public static loadAndParseJsonFile(path: string) {
return JSON.parse( return JSON.parse(
fs.readFileSync( fs.readFileSync(
path, path,
@ -29,28 +40,65 @@ export class SettingsStatic {
) )
} }
public static settingsFileType(path) {
const files = fs.readdirSync(path).filter(fn => fn.startsWith('settings'))
if (files.length) {
const extension = files.pop().split('.')[1]
return FileType[extension]
} else {
return null
}
}
public static loadSettings(path: string, fileType) {
debug('loading settings from %s', path)
switch (fileType) {
case FileType.json: {
debug('settings.json is deprecated, please use a yaml file based on resources/default-settings.yaml')
return this.loadAndParseJsonFile(path)
}
case FileType.yaml: {
return this.loadAndParseYamlFile(path)
}
default: {
throw new Error('settings file was missing or did not contain .yaml or .json extensions.')
}
}
}
public static createSettings(): ISettings { public static createSettings(): ISettings {
if (SettingsStatic._settings) { if (SettingsStatic._settings) {
return SettingsStatic._settings return SettingsStatic._settings
} }
debug('creating settings') debug('creating settings')
const path = SettingsStatic.getSettingsFilePath()
const defaults = defaultSettingsJson as ISettings const basePath = SettingsStatic.getSettingsFileBasePath()
if (!fs.existsSync(basePath)) {
fs.mkdirSync(basePath)
}
const defaultsFilePath = SettingsStatic.getDefaultSettingsFilePath()
const fileType = SettingsStatic.settingsFileType(basePath)
const settingsFilePath = `${basePath}/settings.${fileType}`
const defaults = SettingsStatic.loadSettings(defaultsFilePath, FileType.yaml)
try { try {
if (fs.existsSync(path)) { if (fileType) {
SettingsStatic._settings = mergeDeepRight( SettingsStatic._settings = mergeDeepRight(
defaults, defaults,
SettingsStatic.loadSettings(path) SettingsStatic.loadSettings(settingsFilePath, fileType)
) )
} else { } else {
SettingsStatic.saveSettings(path, defaults) SettingsStatic.saveSettings(basePath, defaults)
SettingsStatic._settings = mergeDeepRight({}, defaults) SettingsStatic._settings = mergeDeepRight({}, defaults)
} }
return SettingsStatic._settings return SettingsStatic._settings
} catch (error) { } catch (error) {
debug('error reading config file at %s: %o', path, error) debug('error reading config file at %s: %o', settingsFilePath, error)
return defaults return defaults
} }
@ -59,9 +107,9 @@ export class SettingsStatic {
public static saveSettings(path: string, settings: ISettings) { public static saveSettings(path: string, settings: ISettings) {
debug('saving settings to %s: %o', path, settings) debug('saving settings to %s: %o', path, settings)
return fs.writeFileSync( return fs.writeFileSync(
path, `${path}/settings.yaml`,
JSON.stringify(settings, null, 2), yaml.dump(settings),
{ encoding: 'utf-8' } { encoding: 'utf-8' },
) )
} }
} }

View File

@ -19,22 +19,45 @@ describe('SettingsStatic', () => {
process.env = originalEnv process.env = originalEnv
}) })
it('returns string ending with settings.json by default', () => { it('returns string ending with .nostr/ by default', () => {
expect(SettingsStatic.getSettingsFilePath()).to.be.a('string').and.to.match(/settings\.json$/) expect(SettingsStatic.getSettingsFileBasePath()).to.be.a('string').and.to.match(/s\.nostr$/)
})
it('returns string ending with given string', () => {
expect(SettingsStatic.getSettingsFilePath('ending')).to.be.a('string').and.to.match(/ending$/)
}) })
it('returns path begins with user\'s home dir by default', () => { it('returns path begins with user\'s home dir by default', () => {
expect(SettingsStatic.getSettingsFilePath()).to.be.a('string').and.equal(`${join(homedir(), '.nostr')}/settings.json`) expect(SettingsStatic.getSettingsFileBasePath()).to.be.a('string').and.equal(`${join(homedir(), '.nostr')}/`)
}) })
it('returns path with NOSTR_CONFIG_DIR if set', () => { it('returns path with NOSTR_CONFIG_DIR if set', () => {
process.env.NOSTR_CONFIG_DIR = '/some/path' process.env.NOSTR_CONFIG_DIR = '/some/path'
expect(SettingsStatic.getSettingsFilePath()).to.be.a('string').and.equal('/some/path/settings.json') expect(SettingsStatic.getSettingsFileBasePath()).to.be.a('string').and.equal('/some/path/')
})
})
describe('.getDefaultSettingsFilePath', () => {
let originalEnv: NodeJS.ProcessEnv
beforeEach(() => {
originalEnv = process.env
process.env = {}
})
afterEach(() => {
process.env = originalEnv
})
it('returns string ending with settings.json by default', () => {
expect(SettingsStatic.getDefaultSettingsFilePath()).to.be.a('string').and.to.match(/settings\.yaml$/)
})
it('returns path begins with user\'s home dir by default', () => {
expect(SettingsStatic.getDefaultSettingsFilePath()).to.be.a('string').and.equal(`${join(homedir(), '.nostr')}/settings.yaml`)
})
it('returns path with NOSTR_CONFIG_DIR if set', () => {
process.env.NOSTR_CONFIG_DIR = '/some/path'
expect(SettingsStatic.getDefaultSettingsFilePath()).to.be.a('string').and.equal('/some/path/settings.yaml')
}) })
}) })
@ -52,7 +75,7 @@ describe('SettingsStatic', () => {
it('loads settings from given path', () => { it('loads settings from given path', () => {
readFileSyncStub.returns('"content"') readFileSyncStub.returns('"content"')
expect(SettingsStatic.loadSettings('/some/path')).to.equal('content') expect(SettingsStatic.loadSettings('/some/path', 'yaml')).to.equal('content')
expect(readFileSyncStub).to.have.been.calledOnceWithExactly( expect(readFileSyncStub).to.have.been.calledOnceWithExactly(
'/some/path', '/some/path',
@ -75,7 +98,7 @@ describe('SettingsStatic', () => {
sandbox = Sinon.createSandbox() sandbox = Sinon.createSandbox()
existsSyncStub = sandbox.stub(fs, 'existsSync') existsSyncStub = sandbox.stub(fs, 'existsSync')
getSettingsFilePathStub = sandbox.stub(SettingsStatic, 'getSettingsFilePath') getSettingsFilePathStub = sandbox.stub(SettingsStatic, 'getSettingsFileBasePath')
saveSettingsStub = sandbox.stub(SettingsStatic, 'saveSettings') saveSettingsStub = sandbox.stub(SettingsStatic, 'saveSettings')
loadSettingsStub = sandbox.stub(SettingsStatic, 'loadSettings') loadSettingsStub = sandbox.stub(SettingsStatic, 'loadSettings')
}) })