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) => {
135 let raw = tokio::fs::read_to_string(path).await?;
136 raw.trim().to_string()
138 }
139 Secret::Value(secret) => secret.clone(),
140 })
141 }
142
143 pub(crate) fn generate<R>(mut rng: R) -> Self
144 where
145 R: Rng + Send,
146 {
147 Self {
148 kind: HomeserverKind::default(),
149 homeserver: default_homeserver(),
150 secret: Secret::Value(Alphanumeric.sample_string(&mut rng, 32)),
151 endpoint: default_endpoint(),
152 }
153 }
154
155 pub(crate) fn test() -> Self {
156 Self {
157 kind: HomeserverKind::default(),
158 homeserver: default_homeserver(),
159 secret: Secret::Value("test".to_owned()),
160 endpoint: default_endpoint(),
161 }
162 }
163}
164
165#[cfg(test)]
166mod tests {
167 use figment::{
168 Figment, Jail,
169 providers::{Format, Yaml},
170 };
171 use tokio::{runtime::Handle, task};
172
173 use super::*;
174
175 #[tokio::test]
176 async fn load_config() {
177 task::spawn_blocking(|| {
178 Jail::expect_with(|jail| {
179 jail.create_file(
180 "config.yaml",
181 r"
182 matrix:
183 homeserver: matrix.org
184 secret_file: secret
185 ",
186 )?;
187 jail.create_file("secret", r"m472!x53c237")?;
188
189 let config = Figment::new()
190 .merge(Yaml::file("config.yaml"))
191 .extract_inner::<MatrixConfig>("matrix")?;
192
193 Handle::current().block_on(async move {
194 assert_eq!(&config.homeserver, "matrix.org");
195 assert!(matches!(config.secret, Secret::File(ref p) if p == "secret"));
196 assert_eq!(config.secret().await.unwrap(), "m472!x53c237");
197 });
198
199 Ok(())
200 });
201 })
202 .await
203 .unwrap();
204 }
205
206 #[tokio::test]
207 async fn load_config_inline_secrets() {
208 task::spawn_blocking(|| {
209 Jail::expect_with(|jail| {
210 jail.create_file(
211 "config.yaml",
212 r"
213 matrix:
214 homeserver: matrix.org
215 secret: m472!x53c237
216 ",
217 )?;
218
219 let config = Figment::new()
220 .merge(Yaml::file("config.yaml"))
221 .extract_inner::<MatrixConfig>("matrix")?;
222
223 Handle::current().block_on(async move {
224 assert_eq!(&config.homeserver, "matrix.org");
225 assert!(matches!(config.secret, Secret::Value(ref v) if v == "m472!x53c237"));
226 assert_eq!(config.secret().await.unwrap(), "m472!x53c237");
227 });
228
229 Ok(())
230 });
231 })
232 .await
233 .unwrap();
234 }
235}