gnostr_relay/
setting.rs

1use crate::Error;
2use crate::{duration::NonZeroDuration, hash::NoOpHasherDefault, Result};
3use config::{Config, Environment, File, FileFormat};
4use notify::{event::ModifyKind, Event, EventKind, RecommendedWatcher, RecursiveMode, Watcher};
5use parking_lot::RwLock;
6use serde::de::DeserializeOwned;
7use serde::{Deserialize, Serialize};
8use serde_json::{json, Value};
9use std::{
10    any::{Any, TypeId},
11    collections::HashMap,
12    fs,
13    ops::Deref,
14    path::{Path, PathBuf},
15    sync::Arc,
16    time::Duration,
17};
18use tracing::{error, info};
19
20pub const CARGO_PKG_VERSION: Option<&'static str> = option_env!("CARGO_PKG_VERSION");
21
22fn default_version() -> String {
23    CARGO_PKG_VERSION.map(ToOwned::to_owned).unwrap_or_default()
24}
25
26fn default_nips() -> Vec<u32> {
27    vec![
28        0, 1, 2, 4, 9, 11, 12, 15, 16, 20, 22, 25, 26, 28, 33, 40, 70, /*nip-0034*/ 30618,
29        30617, 1633, 1632, 1631, 1630, 1621, 1617,
30    ]
31}
32
33#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
34#[serde(default)]
35pub struct Information {
36    pub name: String,
37    pub description: String,
38    pub pubkey: Option<String>,
39    pub contact: Option<String>,
40    pub software: String,
41    #[serde(skip_deserializing)]
42    pub version: String,
43    #[serde(skip_deserializing)]
44    pub supported_nips: Vec<u32>,
45}
46
47impl Default for Information {
48    fn default() -> Self {
49        Self {
50            name: Default::default(),
51            description: Default::default(),
52            pubkey: Default::default(),
53            contact: Default::default(),
54            software: Default::default(),
55            version: default_version(),
56            supported_nips: default_nips(),
57        }
58    }
59}
60
61#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
62#[serde(default)]
63pub struct Data {
64    pub path: PathBuf,
65
66    /// Query filter timeout time
67    pub db_query_timeout: Option<NonZeroDuration>,
68}
69
70impl Default for Data {
71    fn default() -> Self {
72        Self {
73            path: PathBuf::from("./data"),
74            db_query_timeout: None,
75        }
76    }
77}
78
79/// number of threads config
80#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Default)]
81#[serde(default)]
82pub struct Thread {
83    /// number of http server threads
84    pub http: usize,
85    /// number of read event threads
86    pub reader: usize,
87}
88
89/// network config
90#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
91#[serde(default)]
92pub struct Network {
93    /// server bind host
94    pub host: String,
95    /// server bind port
96    pub port: u16,
97    /// heartbeat timeout (default 120 seconds, must bigger than heartbeat interval)
98    /// How long before lack of client response causes a timeout
99    pub heartbeat_timeout: NonZeroDuration,
100
101    /// heartbeat interval
102    /// How often heartbeat pings are sent
103    pub heartbeat_interval: NonZeroDuration,
104
105    pub real_ip_header: Option<String>,
106
107    /// redirect to other site when user access the http index page
108    pub index_redirect_to: Option<String>,
109}
110
111impl Default for Network {
112    fn default() -> Self {
113        Self {
114            host: "127.0.0.1".to_string(),
115            port: 8080,
116            heartbeat_interval: Duration::from_secs(60).try_into().unwrap(),
117            heartbeat_timeout: Duration::from_secs(120).try_into().unwrap(),
118            real_ip_header: None,
119            index_redirect_to: None,
120        }
121    }
122}
123
124#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
125#[serde(default)]
126pub struct Limitation {
127    /// this is the maximum number of bytes for incoming JSON. default 512K
128    pub max_message_length: usize,
129    /// total number of subscriptions that may be active on a single websocket connection to this relay. default 20
130    pub max_subscriptions: usize,
131    /// maximum number of filter values in each subscription. default 10
132    pub max_filters: usize,
133    /// the relay server will clamp each filter's limit value to this number. This means the client won't be able to get more than this number of events from a single subscription filter. default 300
134    pub max_limit: u64,
135    /// maximum length of subscription id as a string. default 100
136    pub max_subid_length: usize,
137    /// for authors and ids filters which are to match against a hex prefix, you must provide at least this many hex digits in the prefix. default 10
138    pub min_prefix: usize,
139    /// in any event, this is the maximum number of elements in the tags list. default 5000
140    pub max_event_tags: usize,
141    /// Events older than this will be rejected. default 3 years, 0 ignore
142    pub max_event_time_older_than_now: u64,
143    /// Events newer than this will be rejected. default 15 minutes, 0 ignore
144    pub max_event_time_newer_than_now: u64,
145}
146
147impl Default for Limitation {
148    fn default() -> Self {
149        Self {
150            max_message_length: 524288,
151            max_subscriptions: 20,
152            max_filters: 10,
153            max_limit: 300,
154            max_subid_length: 100,
155            min_prefix: 10,
156            max_event_tags: 5000,
157            max_event_time_older_than_now: 94608000,
158            max_event_time_newer_than_now: 900,
159        }
160    }
161}
162
163#[derive(Debug, Serialize, Deserialize, Default)]
164#[serde(default)]
165pub struct Setting {
166    pub information: Information,
167    pub data: Data,
168    pub thread: Thread,
169    pub network: Network,
170    pub limitation: Limitation,
171
172    /// flatten extensions setting to json::Value
173    #[serde(flatten)]
174    pub extra: HashMap<String, Value>,
175
176    /// extensions setting object
177    #[serde(skip)]
178    extensions: HashMap<TypeId, Box<dyn Any + Send + Sync>, NoOpHasherDefault>,
179
180    /// nip-11 extension information
181    #[serde(skip)]
182    ext_information: HashMap<String, Value>,
183
184    /// nip-11 extension limitation
185    #[serde(skip)]
186    ext_limitation: HashMap<String, Value>,
187}
188
189impl PartialEq for Setting {
190    fn eq(&self, other: &Self) -> bool {
191        self.information == other.information
192            && self.data == other.data
193            && self.thread == other.thread
194            && self.network == other.network
195            && self.limitation == other.limitation
196            && self.extra == other.extra
197    }
198}
199
200#[derive(Debug, Clone)]
201pub struct SettingWrapper {
202    inner: Arc<RwLock<Setting>>,
203    watcher: Option<Arc<RecommendedWatcher>>,
204}
205
206impl Deref for SettingWrapper {
207    type Target = Arc<RwLock<Setting>>;
208    fn deref(&self) -> &Self::Target {
209        &self.inner
210    }
211}
212
213impl From<Setting> for SettingWrapper {
214    fn from(setting: Setting) -> Self {
215        Self {
216            inner: Arc::new(RwLock::new(setting)),
217            watcher: None,
218        }
219    }
220}
221
222impl SettingWrapper {
223    /// reload setting from file
224    pub fn reload<P: AsRef<Path>>(&self, file: P, env_prefix: Option<String>) -> Result<()> {
225        let setting = Setting::read(&file, env_prefix)?;
226        {
227            let mut w = self.write();
228            *w = setting;
229        }
230        Ok(())
231    }
232
233    /// config from file and watch file update then reload
234    pub fn watch<P: AsRef<Path>, F: Fn(&SettingWrapper) + Send + 'static>(
235        file: P,
236        env_prefix: Option<String>,
237        f: F,
238    ) -> Result<Self> {
239        let mut setting: SettingWrapper = Setting::read(&file, env_prefix.clone())?.into();
240        let c_setting = setting.clone();
241
242        // let file = current_dir()?.join(file.as_ref());
243        // symbolic links
244        let file = fs::canonicalize(file.as_ref())?;
245        let c_file = file.clone();
246
247        // support vim editor. watch dir
248        // https://docs.rs/notify/latest/notify/#editor-behaviour
249        // https://github.com/notify-rs/notify/issues/113#issuecomment-281836995
250
251        let dir = file
252            .parent()
253            .ok_or_else(|| Error::Message("failed to get config dir".to_owned()))?;
254
255        let mut watcher = RecommendedWatcher::new(
256            move |result: Result<Event, notify::Error>| match result {
257                Ok(event) => {
258                    #[cfg(target_os = "windows")]
259                    // There is no distinction between data writes or metadata writes. Both of these are represented by Modify(Any).
260                    let is_modify = matches!(event.kind, EventKind::Modify(ModifyKind::Any));
261                    #[cfg(not(target_os = "windows"))]
262                    let is_modify = matches!(event.kind, EventKind::Modify(ModifyKind::Data(_)));
263                    if is_modify && event.paths.contains(&c_file) {
264                        match c_setting.reload(&c_file, env_prefix.clone()) {
265                            Ok(_) => {
266                                info!("Reload config success {:?}", c_file);
267                                info!("{:?}", c_setting.read());
268                                f(&c_setting);
269                            }
270                            Err(e) => {
271                                error!(
272                                    error = e.to_string(),
273                                    "failed to reload config {:?}", c_file
274                                );
275                            }
276                        }
277                    }
278                }
279                Err(e) => {
280                    error!(error = e.to_string(), "failed to watch file {:?}", c_file);
281                }
282            },
283            notify::Config::default(),
284        )?;
285
286        watcher.watch(dir, RecursiveMode::NonRecursive)?;
287        // save watcher
288        setting.watcher = Some(Arc::new(watcher));
289
290        Ok(setting)
291    }
292}
293
294impl Setting {
295    /// add supported nips for nip-11 information
296    pub fn add_nip(&mut self, nip: u32) {
297        if !self.information.supported_nips.contains(&nip) {
298            self.information.supported_nips.push(nip);
299            self.information.supported_nips.sort();
300        }
301    }
302
303    /// add nip-11 extension information
304    pub fn add_information(&mut self, key: String, value: Value) {
305        self.ext_information.insert(key, value);
306    }
307
308    /// add nip-11 extension limitation
309    pub fn add_limitation(&mut self, key: String, value: Value) {
310        self.ext_limitation.insert(key, value);
311    }
312
313    /// Parse extension setting.
314    pub fn parse_extension<T: DeserializeOwned + Default>(&self, key: &str) -> T {
315        self.extra
316            .get(key)
317            .and_then(|v| {
318                let r = serde_json::from_value::<T>(v.clone());
319                if let Err(err) = &r {
320                    error!(error = err.to_string(), "failed to parse {:?} setting", key);
321                }
322                r.ok()
323            })
324            .unwrap_or_default()
325    }
326
327    /// save extension setting
328    pub fn set_extension<T: Send + Sync + 'static>(&mut self, val: T) {
329        self.extensions.insert(TypeId::of::<T>(), Box::new(val));
330    }
331
332    /// get extension setting
333    pub fn get_extension<T: 'static>(&self) -> Option<&T> {
334        self.extensions
335            .get(&TypeId::of::<T>())
336            .and_then(|boxed| boxed.downcast_ref())
337    }
338
339    /// nip-11 information json
340    pub fn render_information(&self) -> Result<String> {
341        let info = &self.information;
342        let mut val = json!({
343            "name": info.name,
344            "description": info.description,
345            "pubkey": info.pubkey,
346            "contact": info.contact,
347            "software": info.software,
348            "version": info.version,
349            "supported_nips": info.supported_nips,
350            "limitation": &self.limitation,
351        });
352        self.ext_limitation.iter().for_each(|(k, v)| {
353            val["limitation"][k] = v.clone();
354        });
355        self.ext_information.iter().for_each(|(k, v)| {
356            val[k] = v.clone();
357        });
358        Ok(serde_json::to_string_pretty(&val)?)
359    }
360
361    /// read config from file and env
362    pub fn read<P: AsRef<Path>>(file: P, env_prefix: Option<String>) -> Result<Self> {
363        let builder = Config::builder();
364        let mut config = builder
365            // Use serde default feature, ignore the following code
366            // // use defaults
367            // .add_source(Config::try_from(&Self::default())?)
368            // override with file contents
369            .add_source(File::with_name(file.as_ref().to_str().unwrap()));
370        if let Some(prefix) = env_prefix {
371            config = config.add_source(Self::env_source(&prefix));
372        }
373
374        let config = config.build()?;
375        let mut setting: Setting = config.try_deserialize()?;
376        setting.correct();
377        Ok(setting)
378    }
379
380    fn env_source(prefix: &str) -> Environment {
381        Environment::with_prefix(prefix)
382            .try_parsing(true)
383            .prefix_separator("_")
384            .separator("__")
385        // .list_separator(" ")
386        // .with_list_parse_key("")
387    }
388
389    /// read config from env
390    pub fn from_env(env_prefix: String) -> Result<Self> {
391        let mut config = Config::builder();
392        config = config.add_source(Self::env_source(&env_prefix));
393        let config = config.build()?;
394        let mut setting: Setting = config.try_deserialize()?;
395        setting.correct();
396        Ok(setting)
397    }
398
399    /// config from str
400    pub fn from_str(s: &str, format: FileFormat) -> Result<Self> {
401        let builder = Config::builder();
402        let config = builder.add_source(File::from_str(s, format)).build()?;
403        let mut setting: Setting = config.try_deserialize()?;
404        setting.correct();
405        Ok(setting)
406    }
407
408    fn correct(&mut self) {
409        if self.network.heartbeat_timeout <= self.network.heartbeat_interval {
410            error!("network heartbeat_timeout must bigger than heartbeat_interval, use defaults");
411            self.network.heartbeat_interval = Duration::from_secs(60).try_into().unwrap();
412            self.network.heartbeat_timeout = Duration::from_secs(120).try_into().unwrap();
413        }
414    }
415}
416
417#[cfg(test)]
418mod tests {
419    use super::*;
420    use anyhow::Result;
421    use config::FileFormat;
422    use std::{fs, thread::sleep, time::Duration};
423    use tempfile::Builder;
424
425    #[test]
426    fn der() -> Result<()> {
427        let json = r#"{
428            "network": {"port": 1},
429            "information": {"name": "test"},
430            "data": {},
431            "thread": {"http": 1},
432            "limitation": {}
433        }"#;
434
435        let mut def = Setting::default();
436        def.network.port = 1;
437        def.information.name = "test".to_owned();
438        def.thread.http = 1;
439
440        let s2 = serde_json::from_str::<Setting>(json)?;
441        let s1: Setting = Setting::from_str(json, FileFormat::Json)?;
442
443        assert_eq!(def, s1);
444        assert_eq!(def, s2);
445
446        Ok(())
447    }
448
449    #[test]
450    fn render() -> Result<()> {
451        let mut def = Setting::default();
452        def.add_nip(1234567);
453        def.add_limitation("payment_required".to_owned(), json!(true));
454        def.add_information("payments_url".to_owned(), json!("https://payments"));
455        let info = def.render_information()?;
456        let val: Value = serde_json::from_str(&info)?;
457        // println!("{:?}", info);
458        assert!(val["supported_nips"]
459            .as_array()
460            .unwrap()
461            .contains(&Value::Number(serde_json::Number::from(1234567))));
462        assert_eq!(val["payments_url"], json!("https://payments"));
463        assert_eq!(val["limitation"]["payment_required"], json!(true));
464        Ok(())
465    }
466
467    #[test]
468    fn read() -> Result<()> {
469        let setting = Setting::default();
470        assert_eq!(setting.information.name, "");
471        assert!(setting.information.supported_nips.contains(&1));
472
473        let file = Builder::new()
474            .prefix("nostr-relay-config-test-read")
475            .suffix(".toml")
476            .rand_bytes(0)
477            .tempfile()?;
478
479        let setting = Setting::read(&file, None)?;
480        assert_eq!(setting.information.name, "");
481        assert!(setting.information.supported_nips.contains(&1));
482        fs::write(&file, CONFIG)?;
483
484        temp_env::with_vars(
485            [
486                ("NOSTR_information.description", Some("test")),
487                ("NOSTR_information__contact", Some("test")),
488                ("NOSTR_INFORMATION__PUBKEY", Some("test")),
489                ("NOSTR_NETWORK__PORT", Some("1")),
490            ],
491            || {
492                let setting = Setting::read(&file, Some("NOSTR".to_owned())).unwrap();
493                assert_eq!(setting.information.name, "gnostr-relay".to_string());
494                assert_eq!(setting.information.description, "test".to_string());
495                assert_eq!(setting.information.contact, Some("test".to_string()));
496                assert_eq!(setting.information.pubkey, Some("test".to_string()));
497                assert_eq!(setting.network.port, 1);
498            },
499        );
500        Ok(())
501    }
502
503    #[test]
504    fn from_config_str() -> Result<()> {
505        let setting = Setting::from_str(CONFIG, FileFormat::Toml)?;
506        assert_eq!(setting.information.name, "gnostr-relay".to_string());
507        assert_eq!(setting.network.port, 8080);
508        Ok(())
509    }
510
511    #[test]
512    fn watch() -> Result<()> {
513        let file = Builder::new()
514            .prefix("nostr-relay-config-test-watch")
515            .suffix(".toml")
516            .tempfile()?;
517
518        let setting = SettingWrapper::watch(&file, None, |_s| {})?;
519        {
520            let r = setting.read();
521            assert_eq!(r.information.name, "");
522            assert!(r.information.supported_nips.contains(&1));
523        }
524
525        fs::write(
526            &file,
527            r#"[information]
528    name = "nostr"
529    "#,
530        )?;
531        sleep(Duration::from_secs(1));
532        // println!("read {:?} {:?}", setting.read(), file);
533        {
534            let r = setting.read();
535            assert_eq!(r.information.name, "nostr");
536            assert!(r.information.supported_nips.contains(&1));
537        }
538        Ok(())
539    }
540
541    #[test]
542    fn reload() -> Result<()> {
543        let file = Builder::new()
544            .prefix("nostr-relay-config-test-reload")
545            .suffix(".toml")
546            .tempfile()?;
547
548        fs::write(
549            &file,
550            r#"[information]
551    name = "initial"
552    "#,
553        )?;
554
555        let setting_wrapper: SettingWrapper = Setting::read(&file, None)?.into();
556        {
557            let r = setting_wrapper.read();
558            assert_eq!(r.information.name, "initial");
559        }
560
561        fs::write(
562            &file,
563            r#"[information]
564    name = "reloaded"
565    "#,
566        )?;
567
568        setting_wrapper.reload(&file, None)?;
569        {
570            let r = setting_wrapper.read();
571            assert_eq!(r.information.name, "reloaded");
572        }
573        Ok(())
574    }
575
576    #[test]
577    fn correct_heartbeat_duration() -> Result<()> {
578        let json_valid = r#"{
579            "network": {
580                "heartbeat_interval": "60s",
581                "heartbeat_timeout": "120s"
582            }
583        }"#;
584        let setting_valid = Setting::from_str(json_valid, FileFormat::Json)?;
585        assert_eq!(
586            setting_valid.network.heartbeat_interval,
587            Duration::from_secs(60).try_into().unwrap()
588        );
589        assert_eq!(
590            setting_valid.network.heartbeat_timeout,
591            Duration::from_secs(120).try_into().unwrap()
592        );
593
594        let json_invalid = r#"{
595            "network": {
596                "heartbeat_interval": "120s",
597                "heartbeat_timeout": "60s"
598            }
599        }"#;
600        let setting_invalid = Setting::from_str(json_invalid, FileFormat::Json)?;
601        // Should revert to default values if invalid
602        assert_eq!(
603            setting_invalid.network.heartbeat_interval,
604            Duration::from_secs(60).try_into().unwrap()
605        );
606        assert_eq!(
607            setting_invalid.network.heartbeat_timeout,
608            Duration::from_secs(120).try_into().unwrap()
609        );
610        Ok(())
611    }
612
613    #[test]
614    fn from_env() -> Result<()> {
615        temp_env::with_vars(
616            [
617                ("NOSTR_INFORMATION__NAME", Some("env_name")),
618                ("NOSTR_NETWORK__PORT", Some("9000")),
619            ],
620            || {
621                let setting = Setting::from_env("NOSTR".to_owned()).unwrap();
622                assert_eq!(setting.information.name, "env_name".to_string());
623                assert_eq!(setting.network.port, 9000);
624            },
625        );
626        Ok(())
627    }
628
629    const CONFIG: &str = r#"
630# Configuration
631# All duration format reference https://docs.rs/duration-str/latest/duration_str/
632#
633# config relay information
634[information]
635name = "gnostr-relay"
636description = "GnostrApp:a git+nostr workflow utility"
637software = "https://github.com/gnostr-org/gnostr"
638# pubkey = ""
639# contact = ""
640
641# config data path
642[data]
643# the data path (restart required)
644# the events db path is $path/events
645path = "./data"
646
647# Query filter timeout time, default no timeout.
648db_query_timeout = "100ms"
649
650# config network
651[network]
652# Interface to listen on. Use 0.0.0.0 to listen on all interfaces (restart required)
653host = "127.0.0.1"
654# Listen port (restart required)
655port = 8080
656
657# real ip header (default empty)
658# ie: cf-connecting-ip, x-real-ip, x-forwarded-for
659# real_ip_header = "x-forwarded-for"
660
661# redirect to other site when user access the http index page
662index_redirect_to = "https://gnostr.org"
663
664# heartbeat timeout (default 120 seconds, must bigger than heartbeat interval)
665# How long before lack of client response causes a timeout
666# heartbeat_timeout = "2m"
667
668# heartbeat interval (default 60 seconds)
669# How often heartbeat pings are sent
670heartbeat_interval = "1m"
671
672# config thread (restart required)
673[thread]
674# number of http server threads (restart required)
675# default 0 will use the num of cpus
676http = 0
677
678# number of read event threads (restart required)
679# default 0 will use the num of cpus
680reader = 0
681
682[limitation]
683# this is the maximum number of bytes for incoming JSON. default 512K
684max_message_length = 524288
685# total number of subscriptions that may be active on a single websocket connection to this relay. default 20
686max_subscriptions = 20
687# maximum number of filter values in each subscription. default 10
688max_filters = 10
689# the relay server will clamp each filter's limit value to this number. This means the client won't be able to get more than this number of events from a single subscription filter. default 300
690max_limit = 300
691# maximum length of subscription id as a string. default 100
692max_subid_length = 100
693# for authors and ids filters which are to match against a hex prefix, you must provide at least this many hex digits in the prefix. default 10
694min_prefix = 10
695# in any event, this is the maximum number of elements in the tags list. default 5000
696max_event_tags = 5000
697# Events older than this will be rejected. default 3 years
698## max_event_time_older_than_now = 94608000
699# Events newer than this will be rejected. default 15 minutes
700## max_event_time_newer_than_now = 900
701
702# Metrics extension, get the metrics data from https://example.com/metrics?auth=auth_key
703[metrics]
704enabled = true
705# change the auth key
706auth = "auth_key"
707
708# Auth extension
709[auth]
710enabled = false
711
712# # Authenticate the command 'REQ' get event, subscribe filter
713# [auth.req]
714# # only the list IP are allowed to req
715# ip_whitelist = ["127.0.0.1"]
716# # only the list IP are denied to req
717# ip_blacklist = ["127.0.0.1"]
718# # Restrict on nip42 verified pubkey, so client needs to implement nip42 and authenticate success
719# pubkey_whitelist = ["xxxxxx"]
720# pubkey_blacklist = ["xxxx"]
721
722# # Authenticate the command 'EVENT' write event
723# [auth.event]
724# ip_whitelist = ["127.0.0.1"]
725# ip_blacklist = ["127.0.0.1"]
726# # Restrict on nip42 verified pubkey, so client needs to implement nip42 and authenticate success
727# pubkey_whitelist = ["xxxxxx"]
728# pubkey_blacklist = ["xxxx"]
729# # Restrict on event author pubkey, No need nip42 authentication
730# event_pubkey_whitelist = ["xxxxxx"]
731# event_pubkey_blacklist = ["xxxx"]
732# allow_mentioning_whitelisted_pubkeys = true
733
734# IP Rate limiter extension
735[rate_limiter]
736enabled = false
737
738# # interval at second for clearing invalid data to free up memory.
739# # 0 will be converted to default 60 seconds
740# clear_interval = "60s"
741
742# # rate limiter ruler list when write event per user client IP
743# [[rate_limiter.event]]
744# # name of rate limiter, used by metrics
745# name = "all"
746# # description will notice the user when rate limiter exceeded
747# description = "allow only ten events per minute"
748# period = "1m"
749# limit = 10
750
751# # only limit for kinds
752# # support kind list: [1, 2, 3]
753# # kind ranges included(start) to excluded(end): [[0, 10000], [30000, 40000]]
754# # mixed: [1, 2, [30000, 40000]]
755# kinds = [[0, 40000]]
756
757# # skip when ip in whitelist
758ip_whitelist = ["127.0.0.1"]
759
760# [[rate_limiter.event]]
761# name = "kind 10000"
762# description = "allow only five write events per minute when event kind between 0 to 10000"
763# period = "60s"
764# limit = 5
765# kinds = [[0, 10000]]
766
767# NIP-45 Count extension
768# use carefully. see README.md#count
769[count]
770enabled = false
771
772# NIP-50 Search extension
773# use carefully. see README.md#search
774[search]
775enabled = true
776"#;
777}