mod common;
mod notify;
pub(crate) mod verify;
use crate::endpoints::matrix::common::{login, print_client_debug, ClientInfo, PersistentSession};
use crate::endpoints::matrix::notify::{process_rooms, send_messages};
use crate::endpoints::{Endpoint, EndpointConfig};
use crate::notifications::{Key, ValidatedNotification};
use crate::Error;
use async_trait::async_trait;
use tracing::{error, info};
use serde::Deserialize;
use std::any::Any;
use std::collections::{HashMap, HashSet};
use std::path::PathBuf;
use tokio::sync::broadcast::Receiver;
use tokio::sync::watch;
#[derive(Debug, Deserialize, PartialEq, Eq, Hash, Clone)]
pub(crate) struct MatrixConfigFile {
home_server: String,
username: String,
password: String,
session_store_path: String,
recovery_passphrase: String,
room: Vec<MatrixRoomConfigFile>,
}
#[derive(Debug, Deserialize, PartialEq, Eq, Hash, Clone)]
pub(crate) struct MatrixRoomConfigFile {
room: String,
notifications: Vec<String>,
}
#[derive(Debug, Clone)]
pub struct MatrixEndpoint {
home_server: String,
username: String,
password: String,
session_store_path: PathBuf,
recovery_passphrase: String,
rooms: Vec<MatrixRoom>,
}
#[derive(Debug, Clone)]
pub struct MatrixRoom {
room: String,
notifications: HashSet<String>,
}
impl MatrixConfigFile {
fn rooms(&self) -> HashMap<String, HashSet<String>> {
let mut room_map: HashMap<String, HashSet<String>> = HashMap::new();
for room in &self.room {
match room_map.get(room.room.as_str()) {
None => room_map.insert(room.room.to_string(), room.notifications()),
Some(notifications) => {
let new_notifications = room.notifications();
let union: HashSet<_> = new_notifications.union(notifications).collect();
let union: HashSet<_> = union.into_iter().map(|s| s.to_string()).collect();
room_map.insert(room.room.to_string(), union)
}
};
}
room_map
}
}
impl MatrixRoomConfigFile {
fn notifications(&self) -> HashSet<String> {
let notifications: HashSet<_> = self.notifications.clone().into_iter().collect();
notifications
}
}
#[typetag::deserialize(name = "matrix")]
impl EndpointConfig for MatrixConfigFile {
fn to_endpoint(&self) -> Result<Box<dyn Endpoint + Send>, Error> {
Ok(Box::new(MatrixEndpoint::try_from(self)?))
}
}
impl MatrixEndpoint {
pub fn new<S: AsRef<str>>(
home_server: S,
username: S,
password: S,
session_store_path: S,
recovery_passphrase: S,
rooms: Vec<MatrixRoom>,
) -> Self {
let home_server = home_server.as_ref().into();
let username = username.as_ref().into();
let password = password.as_ref().into();
let session_store_path = PathBuf::from(session_store_path.as_ref());
let recovery_passphrase = recovery_passphrase.as_ref().into();
Self { home_server, username, password, session_store_path, recovery_passphrase, rooms }
}
pub fn home_server(&self) -> &str {
&self.home_server
}
pub fn username(&self) -> &str {
&self.username
}
pub fn password(&self) -> &str {
&self.password
}
pub fn session_store_path(&self) -> &PathBuf {
&self.session_store_path
}
pub fn recovery_passphrase(&self) -> &str {
&self.recovery_passphrase
}
pub fn rooms(&self) -> &[MatrixRoom] {
&self.rooms
}
}
impl TryFrom<&MatrixConfigFile> for MatrixEndpoint {
type Error = Error;
fn try_from(value: &MatrixConfigFile) -> Result<Self, Self::Error> {
if value.home_server.is_empty() {
return Err(Error::invalid_endpoint_configuration("Matrix configuration home_server is blank".to_string()));
}
if value.username.is_empty() {
return Err(Error::invalid_endpoint_configuration("Matrix configuration username is blank".to_string()));
}
if value.room.is_empty() {
return Err(Error::invalid_endpoint_configuration("Matrix configuration has no rooms setup".to_string()));
}
let rooms = {
let mut rooms: Vec<_> = Vec::new();
for (room, notifications) in value.rooms() {
rooms.push(MatrixRoom::new(room, notifications));
}
rooms
};
Ok(MatrixEndpoint::new(
value.home_server.as_str(),
value.username.as_str(),
value.password.as_str(),
value.session_store_path.as_str(),
value.recovery_passphrase.as_str(),
rooms,
))
}
}
impl MatrixRoom {
pub fn new(room: String, notifications: HashSet<String>) -> Self {
Self { room, notifications }
}
pub fn room(&self) -> &str {
&self.room
}
pub fn notifications(&self) -> &HashSet<String> {
&self.notifications
}
}
#[async_trait]
impl Endpoint for MatrixEndpoint {
async fn notify(
&self,
endpoint_rx: Receiver<ValidatedNotification>,
shutdown: watch::Receiver<bool>,
) -> Result<(), Error> {
let client_info = ClientInfo::try_from(self)?;
info!(
"Setting up Endpoint: Matrix -> User {} on {}",
client_info.username(),
client_info.homeserver()
);
let client = login(client_info.clone()).await?;
print_client_debug(&client).await;
let room_list = process_rooms(&client, self.rooms()).await;
tokio::spawn(async move {
let sync_token = send_messages(endpoint_rx, shutdown.clone(), room_list, &client).await;
let persist =
PersistentSession::new(&client_info, &client.matrix_auth().session().unwrap(), Some(sync_token));
if let Err(error) = persist.save_session() {
error!("{}", error)
}
});
Ok(())
}
fn generate_keys(&self, hash_key: &Key) -> HashMap<String, HashSet<Key>> {
let mut keys: HashMap<String, HashSet<Key>> = HashMap::new();
for room in self.rooms() {
let mut room_keys = HashSet::new();
for notification_name in room.notifications() {
room_keys.insert(Key::generate(notification_name, hash_key));
}
keys.insert(room.room().to_string(), room_keys);
}
keys
}
fn as_any(&self) -> &dyn Any {
self
}
}