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