mas_config/sections/
matrix.rs1use anyhow::bail;
8use camino::Utf8PathBuf;
9use rand::{
10 Rng,
11 distributions::{Alphanumeric, DistString},
12};
13use schemars::JsonSchema;
14use serde::{Deserialize, Serialize};
15use serde_with::serde_as;
16use url::Url;
17
18use super::ConfigurationSection;
19
20fn default_homeserver() -> String {
21 "localhost:8008".to_owned()
22}
23
24fn default_endpoint() -> Url {
25 Url::parse("http://localhost:8008/").unwrap()
26}
27
28#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default)]
30#[serde(rename_all = "snake_case")]
31pub enum HomeserverKind {
32 #[default]
34 Synapse,
35
36 SynapseReadOnly,
41
42 SynapseLegacy,
44
45 SynapseModern,
47}
48
49#[derive(Clone, Debug)]
54pub enum Secret {
55 File(Utf8PathBuf),
56 Value(String),
57}
58
59#[derive(JsonSchema, Serialize, Deserialize, Clone, Debug)]
61struct SecretRaw {
62 #[schemars(with = "Option<String>")]
63 #[serde(skip_serializing_if = "Option::is_none")]
64 secret_file: Option<Utf8PathBuf>,
65 #[serde(skip_serializing_if = "Option::is_none")]
66 secret: Option<String>,
67}
68
69impl TryFrom<SecretRaw> for Secret {
70 type Error = anyhow::Error;
71
72 fn try_from(value: SecretRaw) -> Result<Self, Self::Error> {
73 match (value.secret, value.secret_file) {
74 (None, None) => bail!("Missing `secret` or `secret_file`"),
75 (None, Some(path)) => Ok(Secret::File(path)),
76 (Some(secret), None) => Ok(Secret::Value(secret)),
77 (Some(_), Some(_)) => bail!("Cannot specify both `secret` and `secret_file`"),
78 }
79 }
80}
81
82impl From<Secret> for SecretRaw {
83 fn from(value: Secret) -> Self {
84 match value {
85 Secret::File(path) => SecretRaw {
86 secret_file: Some(path),
87 secret: None,
88 },
89 Secret::Value(secret) => SecretRaw {
90 secret_file: None,
91 secret: Some(secret),
92 },
93 }
94 }
95}
96
97#[serde_as]
99#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
100pub struct MatrixConfig {
101 #[serde(default)]
103 pub kind: HomeserverKind,
104
105 #[serde(default = "default_homeserver")]
107 pub homeserver: String,
108
109 #[schemars(with = "SecretRaw")]
111 #[serde_as(as = "serde_with::TryFromInto<SecretRaw>")]
112 #[serde(flatten)]
113 pub secret: Secret,
114
115 #[serde(default = "default_endpoint")]
117 pub endpoint: Url,
118}
119
120impl ConfigurationSection for MatrixConfig {
121 const PATH: Option<&'static str> = Some("matrix");
122}
123
124impl MatrixConfig {
125 pub async fn secret(&self) -> anyhow::Result<String> {
133 Ok(match &self.secret {
134 Secret::File(path) => tokio::fs::read_to_string(path).await?,
135 Secret::Value(secret) => secret.clone(),
136 })
137 }
138
139 pub(crate) fn generate<R>(mut rng: R) -> Self
140 where
141 R: Rng + Send,
142 {
143 Self {
144 kind: HomeserverKind::default(),
145 homeserver: default_homeserver(),
146 secret: Secret::Value(Alphanumeric.sample_string(&mut rng, 32)),
147 endpoint: default_endpoint(),
148 }
149 }
150
151 pub(crate) fn test() -> Self {
152 Self {
153 kind: HomeserverKind::default(),
154 homeserver: default_homeserver(),
155 secret: Secret::Value("test".to_owned()),
156 endpoint: default_endpoint(),
157 }
158 }
159}
160
161#[cfg(test)]
162mod tests {
163 use figment::{
164 Figment, Jail,
165 providers::{Format, Yaml},
166 };
167 use tokio::{runtime::Handle, task};
168
169 use super::*;
170
171 #[tokio::test]
172 async fn load_config() {
173 task::spawn_blocking(|| {
174 Jail::expect_with(|jail| {
175 jail.create_file(
176 "config.yaml",
177 r"
178 matrix:
179 homeserver: matrix.org
180 secret_file: secret
181 ",
182 )?;
183 jail.create_file("secret", r"m472!x53c237")?;
184
185 let config = Figment::new()
186 .merge(Yaml::file("config.yaml"))
187 .extract_inner::<MatrixConfig>("matrix")?;
188
189 Handle::current().block_on(async move {
190 assert_eq!(&config.homeserver, "matrix.org");
191 assert!(matches!(config.secret, Secret::File(ref p) if p == "secret"));
192 assert_eq!(config.secret().await.unwrap(), "m472!x53c237");
193 });
194
195 Ok(())
196 });
197 })
198 .await
199 .unwrap();
200 }
201
202 #[tokio::test]
203 async fn load_config_inline_secrets() {
204 task::spawn_blocking(|| {
205 Jail::expect_with(|jail| {
206 jail.create_file(
207 "config.yaml",
208 r"
209 matrix:
210 homeserver: matrix.org
211 secret: m472!x53c237
212 ",
213 )?;
214
215 let config = Figment::new()
216 .merge(Yaml::file("config.yaml"))
217 .extract_inner::<MatrixConfig>("matrix")?;
218
219 Handle::current().block_on(async move {
220 assert_eq!(&config.homeserver, "matrix.org");
221 assert!(matches!(config.secret, Secret::Value(ref v) if v == "m472!x53c237"));
222 assert_eq!(config.secret().await.unwrap(), "m472!x53c237");
223 });
224
225 Ok(())
226 });
227 })
228 .await
229 .unwrap();
230 }
231}