pass_it_on/endpoints/
matrix.rs

1//! Matrix [`Endpoint`] and [`EndpointConfig`] implementation
2//!
3//! ```toml
4//! [[server.endpoint]]
5//! type = "matrix"
6//! home_server = "example.com"
7//! username = "test1"
8//! password = "password"
9//! session_store_path = '/path/to/session/store/matrix_store'
10//! recovery_passphrase = "recovery_passphrase"
11//!
12//!
13//! [[server.endpoint.room]]
14//! room = "#matrix-room:example.com"
15//! notifications = ["notification_id1"]
16//!
17//! [[server.endpoint.room]]
18//! room = "#another-room:example.com"
19//! notifications = ["notification_id2"]
20//! ```
21
22mod common;
23mod notify;
24pub(crate) mod verify;
25
26use crate::endpoints::matrix::common::{login, print_client_debug, ClientInfo, PersistentSession};
27use crate::endpoints::matrix::notify::{process_rooms, send_messages};
28use crate::endpoints::{Endpoint, EndpointConfig};
29use crate::notifications::{Key, ValidatedNotification};
30use crate::Error;
31use async_trait::async_trait;
32use tracing::{error, info};
33use serde::Deserialize;
34use std::any::Any;
35use std::collections::{HashMap, HashSet};
36use std::path::PathBuf;
37use tokio::sync::broadcast::Receiver;
38use tokio::sync::watch;
39
40/// Data structure to represent the Matrix [`EndpointConfig`].
41#[derive(Debug, Deserialize, PartialEq, Eq, Hash, Clone)]
42pub(crate) struct MatrixConfigFile {
43    home_server: String,
44    username: String,
45    password: String,
46    session_store_path: String,
47    recovery_passphrase: String,
48    room: Vec<MatrixRoomConfigFile>,
49}
50
51#[derive(Debug, Deserialize, PartialEq, Eq, Hash, Clone)]
52pub(crate) struct MatrixRoomConfigFile {
53    room: String,
54    notifications: Vec<String>,
55}
56
57/// Data structure to represent the Matrix [`Endpoint`].
58#[derive(Debug, Clone)]
59pub struct MatrixEndpoint {
60    home_server: String,
61    username: String,
62    password: String,
63    session_store_path: PathBuf,
64    recovery_passphrase: String,
65    rooms: Vec<MatrixRoom>,
66}
67
68/// Data structure to represent a Matrix room.
69#[derive(Debug, Clone)]
70pub struct MatrixRoom {
71    room: String,
72    notifications: HashSet<String>,
73}
74
75impl MatrixConfigFile {
76    fn rooms(&self) -> HashMap<String, HashSet<String>> {
77        let mut room_map: HashMap<String, HashSet<String>> = HashMap::new();
78        for room in &self.room {
79            match room_map.get(room.room.as_str()) {
80                None => room_map.insert(room.room.to_string(), room.notifications()),
81                Some(notifications) => {
82                    let new_notifications = room.notifications();
83                    let union: HashSet<_> = new_notifications.union(notifications).collect();
84                    let union: HashSet<_> = union.into_iter().map(|s| s.to_string()).collect();
85                    room_map.insert(room.room.to_string(), union)
86                }
87            };
88        }
89        room_map
90    }
91}
92
93impl MatrixRoomConfigFile {
94    fn notifications(&self) -> HashSet<String> {
95        let notifications: HashSet<_> = self.notifications.clone().into_iter().collect();
96        notifications
97    }
98}
99
100#[typetag::deserialize(name = "matrix")]
101impl EndpointConfig for MatrixConfigFile {
102    fn to_endpoint(&self) -> Result<Box<dyn Endpoint + Send>, Error> {
103        Ok(Box::new(MatrixEndpoint::try_from(self)?))
104    }
105}
106
107impl MatrixEndpoint {
108    /// Create a new `MatrixEndpoint`.
109    pub fn new<S: AsRef<str>>(
110        home_server: S,
111        username: S,
112        password: S,
113        session_store_path: S,
114        recovery_passphrase: S,
115        rooms: Vec<MatrixRoom>,
116    ) -> Self {
117        let home_server = home_server.as_ref().into();
118        let username = username.as_ref().into();
119        let password = password.as_ref().into();
120        let session_store_path = PathBuf::from(session_store_path.as_ref());
121        let recovery_passphrase = recovery_passphrase.as_ref().into();
122        Self { home_server, username, password, session_store_path, recovery_passphrase, rooms }
123    }
124
125    /// Return the matrix home server.
126    pub fn home_server(&self) -> &str {
127        &self.home_server
128    }
129
130    /// Return the matrix username.
131    pub fn username(&self) -> &str {
132        &self.username
133    }
134
135    /// Return the password for the matrix user.
136    pub fn password(&self) -> &str {
137        &self.password
138    }
139
140    /// Return the path to the persistent session store.
141    pub fn session_store_path(&self) -> &PathBuf {
142        &self.session_store_path
143    }
144
145    /// Return the recovery passphrase.
146    pub fn recovery_passphrase(&self) -> &str {
147        &self.recovery_passphrase
148    }
149
150    /// Return the matrix rooms setup for this matrix endpoint.
151    pub fn rooms(&self) -> &[MatrixRoom] {
152        &self.rooms
153    }
154}
155
156impl TryFrom<&MatrixConfigFile> for MatrixEndpoint {
157    type Error = Error;
158
159    fn try_from(value: &MatrixConfigFile) -> Result<Self, Self::Error> {
160        if value.home_server.is_empty() {
161            return Err(Error::InvalidEndpointConfiguration("Matrix configuration home_server is blank".to_string()));
162        }
163
164        if value.username.is_empty() {
165            return Err(Error::InvalidEndpointConfiguration("Matrix configuration username is blank".to_string()));
166        }
167
168        if value.room.is_empty() {
169            return Err(Error::InvalidEndpointConfiguration("Matrix configuration has no rooms setup".to_string()));
170        }
171
172        let rooms = {
173            let mut rooms: Vec<_> = Vec::new();
174            for (room, notifications) in value.rooms() {
175                rooms.push(MatrixRoom::new(room, notifications));
176            }
177            rooms
178        };
179
180        Ok(MatrixEndpoint::new(
181            value.home_server.as_str(),
182            value.username.as_str(),
183            value.password.as_str(),
184            value.session_store_path.as_str(),
185            value.recovery_passphrase.as_str(),
186            rooms,
187        ))
188    }
189}
190
191impl MatrixRoom {
192    /// Create a new `MatrixRoom`.
193    pub fn new(room: String, notifications: HashSet<String>) -> Self {
194        Self { room, notifications }
195    }
196
197    /// Return the matrix room name.
198    pub fn room(&self) -> &str {
199        &self.room
200    }
201
202    /// Return notification names associated with this room.
203    pub fn notifications(&self) -> &HashSet<String> {
204        &self.notifications
205    }
206}
207
208#[async_trait]
209impl Endpoint for MatrixEndpoint {
210    async fn notify(
211        &self,
212        endpoint_rx: Receiver<ValidatedNotification>,
213        shutdown: watch::Receiver<bool>,
214    ) -> Result<(), Error> {
215        // Login client
216        let client_info = ClientInfo::try_from(self)?;
217        info!(
218            
219            "Setting up Endpoint: Matrix -> User {} on {}",
220            client_info.username(),
221            client_info.homeserver()
222        );
223        let client = login(client_info.clone()).await?;
224
225        print_client_debug(&client).await;
226        let room_list = process_rooms(&client, self.rooms()).await;
227
228        // Monitor for messages to send
229        tokio::spawn(async move {
230            let sync_token = send_messages(endpoint_rx, shutdown.clone(), room_list, &client).await;
231            let persist =
232                PersistentSession::new(&client_info, &client.matrix_auth().session().unwrap(), Some(sync_token));
233            if let Err(error) = persist.save_session() {
234                error!("{}", error)
235            }
236        });
237
238        Ok(())
239    }
240
241    fn generate_keys(&self, hash_key: &Key) -> HashMap<String, HashSet<Key>> {
242        let mut keys: HashMap<String, HashSet<Key>> = HashMap::new();
243
244        for room in self.rooms() {
245            let mut room_keys = HashSet::new();
246            for notification_name in room.notifications() {
247                room_keys.insert(Key::generate(notification_name, hash_key));
248            }
249            keys.insert(room.room().to_string(), room_keys);
250        }
251        keys
252    }
253
254    fn as_any(&self) -> &dyn Any {
255        self
256    }
257}