diff --git a/Cargo.lock b/Cargo.lock index 1bee635..ee77c4a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -323,9 +323,9 @@ dependencies = [ [[package]] name = "async-trait" -version = "0.1.56" +version = "0.1.57" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96cf8829f67d2eab0b2dfa42c5d0ef737e0724e4a82b01b3e292456202b19716" +checksum = "76464446b8bc32758d7e88ee1a804d9914cd9b1cb264c029899680b0be29826f" dependencies = [ "proc-macro2", "quote", @@ -466,9 +466,9 @@ checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" [[package]] name = "bytes" -version = "1.1.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4872d67bab6358e59559027aa3b9157c53d9358c51423c17554809a8858e0f8" +checksum = "ec8a7b6a70fde80372154c65702f00a0f56f3e1c36abbc6c440484be248856db" [[package]] name = "bytestring" @@ -1065,6 +1065,12 @@ dependencies = [ "libc", ] +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + [[package]] name = "hkdf" version = "0.12.3" @@ -2314,9 +2320,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.81" +version = "1.0.85" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b7ce2b32a1aed03c558dc61a5cd328f15aff2dbc17daad8fb8af04d2100e15c" +checksum = "e55a28e3aaef9d5ce0506d0a14dbba8054ddc7e499ef522dd8b26859ec9d4a44" dependencies = [ "itoa", "ryu", @@ -2547,8 +2553,6 @@ dependencies = [ [[package]] name = "threema-gateway" version = "0.15.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5155a6752d66396b91b85b7a2c14bc02cd843212a219db067468ed2b7bc691b7" dependencies = [ "byteorder", "data-encoding", @@ -2565,16 +2569,20 @@ dependencies = [ [[package]] name = "threematrix" -version = "0.1.0" +version = "0.2.0" dependencies = [ "actix-web", + "async-trait", "flexi_logger", "futures", + "hex", "log", "matrix-sdk", + "mime", "rand 0.8.5", "serde", "serde_derive", + "serde_json", "signal-hook", "signal-hook-tokio", "thiserror", diff --git a/Cargo.toml b/Cargo.toml index 0d42ab3..b63a403 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,10 +1,11 @@ [package] name = "threematrix" -version = "0.1.0" +version = "0.2.0" edition = "2021" [dependencies] -threema-gateway = "0.15.1" +#threema-gateway = { git = "https://github.com/bitbetterde/threema-gateway-rs.git" } +threema-gateway = { path = "../threema-gateway-rs" } tokio = { version = "1", features = ["full"] } actix-web = "4" rand = "0.8.5" @@ -17,4 +18,8 @@ signal-hook-tokio = { features = ["futures-v0_3"], version = "0.3.1" } futures = "0.3.21" log = "0.4.17" flexi_logger = "0.22.5" -thiserror="1.0.31" \ No newline at end of file +thiserror="1.0.31" +serde_json = "1.0.83" +hex="0.4.3" +mime = "0.3.16" +async-trait = "0.1.57" diff --git a/src/incoming_message_handler/matrix.rs b/src/incoming_message_handler/matrix.rs new file mode 100644 index 0000000..55d598f --- /dev/null +++ b/src/incoming_message_handler/matrix.rs @@ -0,0 +1,204 @@ +use std::str::FromStr; +use log::{debug, error, info, warn}; +use matrix_sdk::{Client, ruma::events::room::member::StrippedRoomMemberEvent}; +use matrix_sdk::event_handler::Ctx; +use matrix_sdk::media::MediaThumbnailSize; +use matrix_sdk::room::{Joined, Room}; +use matrix_sdk::ruma::{TransactionId, UInt}; +use matrix_sdk::ruma::api::client::media::get_content_thumbnail::v3::Method; +use matrix_sdk::ruma::events::OriginalSyncMessageLikeEvent; +use matrix_sdk::ruma::events::room::message::{MessageType, RoomMessageEventContent, TextMessageEventContent}; +use tokio::time::{Duration, sleep}; + +use crate::matrix::util::get_threematrix_room_state; +use crate::threema::ThreemaClient; +use crate::threema::util::convert_group_id_from_readable_string; + +pub async fn matrix_incoming_message_handler( + event: OriginalSyncMessageLikeEvent, + room: Room, + threema_client: Ctx, + matrix_client: Client, +) -> () { + match room { + Room::Joined(room) => { + match event { + OriginalSyncMessageLikeEvent { + content: + RoomMessageEventContent { + msgtype: MessageType::Text(TextMessageEventContent { body: msg_body, .. }), + .. + }, + sender, + .. + } => + { + debug!("Matrix: Incoming message: {}", msg_body); + + let sender_member = room.get_member(&sender).await; + match sender_member { + Ok(Some(sender_member)) => { + let sender_name = sender_member + .display_name() + .unwrap_or_else(|| sender_member.user_id().as_str()); + + // Filter out messages coming from our own bridge user + if sender != matrix_client.user_id().await.unwrap() { + match get_threematrix_room_state(&room).await { + Ok(None) => { + let err_txt = format!("Room {} does not have proper room state. Have you bound the room to a Threema group?", + &room.display_name().await.unwrap_or(matrix_sdk::DisplayName::Named("UNKNOWN".to_owned()))); + send_error_message_to_matrix_room(&room, err_txt, false).await; + } + Ok(Some(threematrix_state)) => { + let group_id = convert_group_id_from_readable_string( + threematrix_state.threematrix_threema_group_id.as_str(), + ); + + if let Ok(group_id) = group_id { + if let Err(e) = threema_client + .send_group_msg_by_group_id( + format!("*{}*: {}", sender_name, msg_body).as_str(), + group_id.as_slice(), + ) + .await + { + let err_txt = format!( + "Couldn't send message to Threema group: {}", + e + ); + send_error_message_to_matrix_room(&room, err_txt, true) + .await; + } + } + } + Err(e) => { + let err_txt = format!("Could not retrieve room state: {}", e); + send_error_message_to_matrix_room(&room, err_txt, true).await; + } + } + } + } + _ => { + error!("Matrix: Could not resolve room member!"); + } + } + } + OriginalSyncMessageLikeEvent { + content: + RoomMessageEventContent { + msgtype: MessageType::Image(image), + .. + }, + sender, + .. + } => + { + let sender_member = room.get_member(&sender).await; + match sender_member { + Ok(Some(sender_member)) => { + let sender_name = sender_member + .display_name() + .unwrap_or_else(|| sender_member.user_id().as_str()); + + // Filter out messages coming from our own bridge user + if sender != matrix_client.user_id().await.unwrap() { + match get_threematrix_room_state(&room).await { + Ok(None) => { + let err_txt = format!("Room {} does not have proper room state. Have you bound the room to a Threema group?", + &room.display_name().await.unwrap_or(matrix_sdk::DisplayName::Named("UNKNOWN".to_owned()))); + send_error_message_to_matrix_room(&room, err_txt, false).await; + } + Ok(Some(threematrix_state)) => { + let group_id = convert_group_id_from_readable_string( + threematrix_state.threematrix_threema_group_id.as_str(), + ); + + if let Ok(group_id) = group_id { + let image_file = matrix_client.get_file(image.clone(), false).await.unwrap().unwrap(); + let thumbnail = matrix_client.get_thumbnail(image.clone(), MediaThumbnailSize { height: UInt::new(400).unwrap(), width: UInt::new(400).unwrap(), method: Method::Scale }, false).await.unwrap().unwrap(); + + let mime = mime::Mime::from_str(image.info.unwrap().mimetype.unwrap().as_ref()).unwrap(); + + let description = format!("*{}*: {}", sender_name, &image.body); + debug!("Matrix image size: {} bytes", image_file.len()); + debug!("Matrix thumbnail size: {} bytes", thumbnail.len()); + + threema_client.send_group_file_by_group_id( + &image_file, + Some(&thumbnail), + Some(description.as_str()), + image.body.as_str(), + mime, + &group_id + ).await.unwrap(); + } + } + Err(e) => { + let err_txt = format!("Could not retrieve room state: {}", e); + send_error_message_to_matrix_room(&room, err_txt, true).await; + } + } + } + } + _ => { + error!("Matrix: Could not resolve room member!"); + } + } + } + _ => {} + } + } + _ => { + // If bot not member of room, ignore incoming message + } + } +} + + +async fn send_error_message_to_matrix_room(room: &Joined, err_txt: String, log_level_err: bool) { + if log_level_err { + error!("Matrix: {}", err_txt); + } else { + warn!("Matrix: {}", err_txt); + } + + let content = RoomMessageEventContent::text_plain(err_txt.clone()); + let txn_id = TransactionId::new(); + + if let Err(e) = room.send(content, Some(&txn_id)).await { + error!("Matrix: Could not send error message: \"{}\". {}",err_txt, e) + } +} + +// Source: https://github.com/matrix-org/matrix-rust-sdk/blob/matrix-sdk-0.5.0/crates/matrix-sdk/examples/autojoin.rs +pub async fn on_stripped_state_member( + room_member: StrippedRoomMemberEvent, + client: Client, + room: Room, +) { + if room_member.state_key != client.user_id().await.unwrap() { + return; + } + + if let Room::Invited(room) = room { + debug!("Matrix: Autojoining room {}", room.room_id()); + let mut delay = 2; + + while let Err(err) = room.accept_invitation().await { + // retry autojoin due to synapse sending invites, before the + // invited user can join for more information see + // https://github.com/matrix-org/synapse/issues/4345 + error!("Matrix: Failed to join room {} ({:?}), retrying in {}s",room.room_id(), err, delay); + + sleep(Duration::from_secs(delay)).await; + delay *= 2; + + if delay > 3600 { + error!("Matrix: Can't join room {} ({:?})", room.room_id(), err); + break; + } + } + info!("Matrix: Successfully joined room {}", room.room_id()); + } +} diff --git a/src/incoming_message_handler/mod.rs b/src/incoming_message_handler/mod.rs new file mode 100644 index 0000000..af36c26 --- /dev/null +++ b/src/incoming_message_handler/mod.rs @@ -0,0 +1,2 @@ +pub mod threema; +pub mod matrix; \ No newline at end of file diff --git a/src/incoming_message_handler/threema.rs b/src/incoming_message_handler/threema.rs new file mode 100644 index 0000000..ff3266b --- /dev/null +++ b/src/incoming_message_handler/threema.rs @@ -0,0 +1,234 @@ +use actix_web::{http::header::ContentType, web, HttpResponse, Responder}; +use log::{debug, error, info, warn}; +use threema_gateway::IncomingMessage; + +use crate::{AppState, Message}; +use crate::matrix::errors::{BindThreemaGroupToMatrixError, SendToMatrixRoomByThreemaGroupIdError}; +use crate::matrix::MatrixClient; +use crate::threema::ThreemaClient; + +pub async fn threema_incoming_message_handler( + incoming_message: web::Form, + app_state: web::Data, +) -> impl Responder { + let threema_client = &app_state.threema_client; + let decrypted_message = threema_client.process_incoming_msg(&incoming_message).await; + + match decrypted_message { + Ok(message) => match message { + Message::GroupTextMessage(group_text_msg) => { + let matrix_client = app_state.matrix_client.lock().await; + + if group_text_msg.text.starts_with("!threematrix") { + let split_text: Vec<&str> = group_text_msg.text.split(" ").collect(); + match split_text.get(1).map(|str| *str) { + Some("bind") => { + let matrix_room_id = split_text.get(2); + if let Some(matrix_room_id) = matrix_room_id { + match matrix_client.bind_threema_group_to_matrix_room(&group_text_msg.group_id, matrix_room_id).await { + Ok(_) => { + let succ_text = format!("Group has been successfully bound to Matrix room: {}", matrix_room_id); + if let Err(e) = threema_client.send_group_msg_by_group_id( + succ_text.as_str(), group_text_msg.group_id.as_slice()).await + { + error!("Threema: Could not send bind text: {}", e) + } + } + Err(e) => { + match e { + BindThreemaGroupToMatrixError::InvalidGroupId(_) => { + error!("Threema: Group Id not valid!"); + } + BindThreemaGroupToMatrixError::MatrixError(e) => { + let err_text = format!("Could not set Matrix room state: {}", e); + send_error_message_to_threema_group( + threema_client, + err_text, + group_text_msg.group_id.as_slice(), + false, + ).await; + } + BindThreemaGroupToMatrixError::InvalidMatrixRoomId(e) => { + let err_text = format!("Invalid matrix room Id: {}", e); + send_error_message_to_threema_group( + threema_client, + err_text, + group_text_msg.group_id.as_slice(), + false, + ).await; + } + } + } + } + } else { + let err_text = format!("Missing Matrix room id!"); + send_error_message_to_threema_group( + threema_client, + err_text, + group_text_msg.group_id.as_slice(), + false, + ).await; + } + } + Some("help") => { + let help_txt = r#"To bind this Threema Group to a Matrix Room, please use the command "!threematrix bind !abc123:homeserver.org". +You can find the required room id in your Matrix client. Attention: This is NOT a "human readable" room alias, but an "internal" room id, which consists of random characters."#; + if let Err(e) = threema_client + .send_group_msg_by_group_id( + help_txt, + group_text_msg.group_id.as_slice(), + ) + .await + { + error!("Threema: Could not send help text: {}", e) + } + } + _ => { + let err_text = format!( + "Command not found! Use *!threematrix help* for more information" + ); + send_error_message_to_threema_group( + threema_client, + err_text, + group_text_msg.group_id.as_slice(), + false, + ) + .await; + } + } + } else { + let sender_name = group_text_msg + .base + .push_from_name + .unwrap_or("UNKNOWN".to_owned()); + + match matrix_client.get_joined_room_by_threema_group_id(&group_text_msg.group_id).await { + Ok(room) => { + if let Err(e) = matrix_client + .send_message_to_matrix_room( + &room, + sender_name.as_str(), + group_text_msg.text.as_str(), + group_text_msg.text.as_str(), + ).await + { + match e { + SendToMatrixRoomByThreemaGroupIdError::MatrixError(e) => { + let err_txt = format!("Could not send message to Matrix room: {}", e); + send_error_message_to_threema_group( + threema_client, + err_txt, + group_text_msg.group_id.as_slice(), + true, + ).await; + } + } + } + } + Err(_) => { + debug!("No Matrix room for Threema group id found. Maybe group is not bound to any room"); + } + } + } + } + Message::GroupFileMessage(group_file_msg) => { + let matrix_client = app_state.matrix_client.lock().await; + debug!("Threema: Start sending file to Matrix"); + let sender_name = group_file_msg + .base + .push_from_name + .unwrap_or("UNKNOWN".to_owned()); + + match matrix_client.get_joined_room_by_threema_group_id(&group_file_msg.group_id).await { + Ok(room) => { + let file_description = group_file_msg.file_metadata.description().as_deref().unwrap_or(""); + if let Err(e) = matrix_client + .send_message_to_matrix_room( + &room, + sender_name.as_str(), + file_description, + file_description, + ).await + { + match e { + SendToMatrixRoomByThreemaGroupIdError::MatrixError(e) => { + let err_txt = format!("Could not send message to Matrix room: {}", e); + send_error_message_to_threema_group( + threema_client, + err_txt, + group_file_msg.group_id.as_slice(), + true, + ).await; + } + } + } + if let Err(e) = matrix_client + .send_file_to_matrix_room( + &room, + group_file_msg.file_metadata.file_name().as_deref().unwrap_or(""), + group_file_msg.file.as_slice(), + ).await + { + match e { + SendToMatrixRoomByThreemaGroupIdError::MatrixError(e) => { + let err_txt = format!("Could not send message to Matrix room: {}", e); + send_error_message_to_threema_group( + threema_client, + err_txt, + group_file_msg.group_id.as_slice(), + true, + ).await; + } + } + } + } + Err(_) => { + debug!("No Matrix room for Threema group id found. Maybe group is not bound to any room"); + } + } + } + Message::GroupCreateMessage(group_create_msg) => { + info!( + "Got group create message with members: {:?}", + group_create_msg.members + ); + } + Message::GroupRenameMessage(group_rename_msg) => { + info!( + "Got group rename message for: {:?}", + group_rename_msg.group_name + ); + } + _ => {} + }, + Err(err) => { + error!("Threema: Incoming Message Error: {}", err); + } + } + + HttpResponse::Ok() + .content_type(ContentType::plaintext()) + .body(()) +} + +async fn send_error_message_to_threema_group( + threema_client: &ThreemaClient, + err_text: String, + group_id: &[u8], + log_level_error: bool, +) { + if log_level_error { + error!("Threema: {}", err_text); + } else { + warn!("Threema: {}", err_text); + } + if let Err(e) = threema_client + .send_group_msg_by_group_id(err_text.as_str(), group_id) + .await + { + error!( + "Threema: Could not send error message: \"{}\". {}", + err_text, e + ) + } +} diff --git a/src/lib.rs b/src/lib.rs index a8371ad..5e37d0e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,34 +1,18 @@ use std::env::var; use std::fs::read_to_string; -use actix_web::{http::header::ContentType, web, HttpResponse, Responder}; -use log::{debug, error, info, warn}; -use matrix_sdk::event_handler::Ctx; -use matrix_sdk::room::{Joined, Room}; -use matrix_sdk::ruma::events::room::message::{ - MessageType, RoomMessageEventContent, TextMessageEventContent, -}; -use matrix_sdk::ruma::events::OriginalSyncMessageLikeEvent; -use matrix_sdk::ruma::TransactionId; use matrix_sdk::Client; use serde_derive::{Deserialize, Serialize}; -use threema_gateway::IncomingMessage; use tokio::sync::Mutex; use threema::types::Message; - -use crate::matrix::util::{ - get_threematrix_room_state, set_threematrix_room_state, ThreematrixStateEventContent, -}; -use crate::threema::util::{ - convert_group_id_from_readable_string, convert_group_id_to_readable_string, -}; use crate::threema::ThreemaClient; pub mod errors; pub mod matrix; pub mod threema; pub mod util; +pub mod incoming_message_handler; pub struct AppState { pub threema_client: ThreemaClient, @@ -81,299 +65,4 @@ impl ThreematrixConfig { }; return config_from_file; } -} - -pub async fn threema_incoming_message_handler( - incoming_message: web::Form, - app_state: web::Data, -) -> impl Responder { - let threema_client = &app_state.threema_client; - let decrypted_message = threema_client.process_incoming_msg(&incoming_message).await; - - match decrypted_message { - Ok(message) => match message { - Message::GroupTextMessage(group_text_msg) => { - let matrix_client = app_state.matrix_client.lock().await; - - if group_text_msg.text.starts_with("!threematrix") { - let split_text: Vec<&str> = group_text_msg.text.split(" ").collect(); - match split_text.get(1).map(|str| *str) { - Some("bind") => { - let rooms = matrix_client.joined_rooms(); - let matrix_room_id = split_text.get(2); - - if let Some(matrix_room_id) = matrix_room_id { - if let Some(room) = - rooms.iter().find(|r| r.room_id() == matrix_room_id) - { - if let Ok(r) = convert_group_id_to_readable_string( - &group_text_msg.group_id, - ) { - let content: ThreematrixStateEventContent = - ThreematrixStateEventContent { - threematrix_threema_group_id: r, - }; - - if let Err(e) = - set_threematrix_room_state(content, room).await - { - let err_text = - format!("Could not set Matrix room state: {}", e); - send_error_message_to_threema_group( - threema_client, - err_text, - group_text_msg.group_id.as_slice(), - false, - ) - .await; - } else { - let succ_text = format!("Group has been successfully bound to Matrix room: {}", matrix_room_id); - if let Err(e) = threema_client - .send_group_msg_by_group_id( - succ_text.as_str(), - group_text_msg.group_id.as_slice(), - ) - .await - { - error!("Threema: Could not send bind text: {}", e) - } - }; - } else { - error!("Threema: Group Id not valid!"); - } - } else { - let err_text = format!("Matrix room not found. Maybe the bot is not invited or the room id has wrong format!"); - send_error_message_to_threema_group( - threema_client, - err_text, - group_text_msg.group_id.as_slice(), - false, - ) - .await; - } - } else { - let err_text = format!("Missing Matrix room id!"); - send_error_message_to_threema_group( - threema_client, - err_text, - group_text_msg.group_id.as_slice(), - false, - ) - .await; - } - } - Some("help") => { - let help_txt = r#"To bind this Threema Group to a Matrix Room, please use the command "!threematrix bind !abc123:homeserver.org". -You can find the required room id in your Matrix client. Attention: This is NOT a "human readable" room alias, but an "internal" room id, which consists of random characters."#; - if let Err(e) = threema_client - .send_group_msg_by_group_id( - help_txt, - group_text_msg.group_id.as_slice(), - ) - .await - { - error!("Threema: Could not send help text: {}", e) - } - } - _ => { - let err_text = format!( - "Command not found! Use *!threematrix help* for more information" - ); - send_error_message_to_threema_group( - threema_client, - err_text, - group_text_msg.group_id.as_slice(), - false, - ) - .await; - } - } - } else { - let sender_name = group_text_msg - .base - .push_from_name - .unwrap_or("UNKNOWN".to_owned()); - let content = RoomMessageEventContent::text_html( - format!("{}: {}", sender_name, group_text_msg.text.as_str()), - format!( - "{}: {}", - sender_name, - group_text_msg.text.as_str() - ), - ); - for room in matrix_client.joined_rooms() { - match get_threematrix_room_state(&room).await { - Ok(None) => debug!( - "Matrix: Room {:?} does not have proper room state", - &room.display_name().await.unwrap_or( - matrix_sdk::DisplayName::Named("UNKNOWN".to_owned()) - ) - ), - Ok(Some(state)) => { - if let Ok(group_id) = - convert_group_id_to_readable_string(&group_text_msg.group_id) - { - if state.threematrix_threema_group_id == group_id { - let txn_id = TransactionId::new(); - if let Err(e) = - room.send(content.clone(), Some(&txn_id)).await - { - let err_txt = format!( - "Could not send message to Matrix room: {}", - e - ); - send_error_message_to_threema_group( - threema_client, - err_txt, - group_text_msg.group_id.as_slice(), - true, - ) - .await; - } - } - } - } - Err(e) => warn!("Matrix: Could not retrieve room state: {}", e), - } - } - } - } - Message::GroupCreateMessage(group_create_msg) => { - info!( - "Got group create message with members: {:?}", - group_create_msg.members - ); - } - Message::GroupRenameMessage(group_rename_msg) => { - info!( - "Got group rename message for: {:?}", - group_rename_msg.group_name - ); - } - _ => {} - }, - Err(err) => { - error!("Threema: Incoming Message Error: {}", err); - } - } - - HttpResponse::Ok() - .content_type(ContentType::plaintext()) - .body(()) -} - -async fn send_error_message_to_threema_group( - threema_client: &ThreemaClient, - err_text: String, - group_id: &[u8], - log_level_error: bool, -) { - if log_level_error { - error!("Threema: {}", err_text); - } else { - warn!("Threema: {}", err_text); - } - if let Err(e) = threema_client - .send_group_msg_by_group_id(err_text.as_str(), group_id) - .await - { - error!( - "Threema: Could not send error message: \"{}\". {}", - err_text, e - ) - } -} - -pub async fn matrix_incoming_message_handler( - event: OriginalSyncMessageLikeEvent, - room: Room, - threema_client: Ctx, - matrix_client: Client, -) -> () { - match room { - Room::Joined(room) => { - if let OriginalSyncMessageLikeEvent { - content: - RoomMessageEventContent { - msgtype: MessageType::Text(TextMessageEventContent { body: msg_body, .. }), - .. - }, - sender, - .. - } = event - { - debug!("Matrix: Incoming message: {}", msg_body); - - let sender_member = room.get_member(&sender).await; - match sender_member { - Ok(Some(sender_member)) => { - let sender_name = sender_member - .display_name() - .unwrap_or_else(|| sender_member.user_id().as_str()); - - // Filter out messages coming from our own bridge user - if sender != matrix_client.user_id().await.unwrap() { - match get_threematrix_room_state(&room).await { - Ok(None) => { - let err_txt = format!("Room {} does not have proper room state. Have you bound the room to a Threema group?", - &room.display_name().await.unwrap_or(matrix_sdk::DisplayName::Named("UNKNOWN".to_owned()))); - send_error_message_to_matrix_room(&room, err_txt, false).await; - } - Ok(Some(threematrix_state)) => { - let group_id = convert_group_id_from_readable_string( - threematrix_state.threematrix_threema_group_id.as_str(), - ); - - if let Ok(group_id) = group_id { - if let Err(e) = threema_client - .send_group_msg_by_group_id( - format!("*{}*: {}", sender_name, msg_body).as_str(), - group_id.as_slice(), - ) - .await - { - let err_txt = format!( - "Couldn't send message to Threema group: {}", - e - ); - send_error_message_to_matrix_room(&room, err_txt, true) - .await; - } - } - } - Err(e) => { - let err_txt = format!("Could not retrieve room state: {}", e); - send_error_message_to_matrix_room(&room, err_txt, true).await; - } - } - } - } - _ => { - error!("Matrix: Could not resolve room member!"); - } - } - } - } - _ => { - // If bot not member of room, ignore incoming message - } - } -} - -async fn send_error_message_to_matrix_room(room: &Joined, err_txt: String, log_level_err: bool) { - if log_level_err { - error!("Matrix: {}", err_txt); - } else { - warn!("Matrix: {}", err_txt); - } - - let content = RoomMessageEventContent::text_plain(err_txt.clone()); - let txn_id = TransactionId::new(); - - if let Err(e) = room.send(content, Some(&txn_id)).await { - error!( - "Matrix: Could not send error message: \"{}\". {}", - err_txt, e - ) - } -} +} \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index dd16da0..40f8f10 100644 --- a/src/main.rs +++ b/src/main.rs @@ -11,12 +11,10 @@ use std::error::Error; use std::process; use tokio::sync::Mutex; -use threematrix::matrix::on_stripped_state_member; use threematrix::threema::ThreemaClient; -use threematrix::{ - matrix_incoming_message_handler, threema_incoming_message_handler, AppState, LoggerConfig, - ThreematrixConfig, -}; +use threematrix::{AppState, LoggerConfig, ThreematrixConfig}; +use threematrix::incoming_message_handler::matrix::{matrix_incoming_message_handler, on_stripped_state_member}; +use threematrix::incoming_message_handler::threema::threema_incoming_message_handler; const VERSION: &str = env!("CARGO_PKG_VERSION"); const CRATE_NAME: &str = env!("CARGO_CRATE_NAME"); @@ -89,11 +87,11 @@ async fn main() -> Result<(), Box> { web::post().to(threema_incoming_message_handler), ) }) - .bind(( - cfg.threema.host.unwrap_or("localhost".to_owned()), - cfg.threema.port.unwrap_or(443), - ))? - .run(), + .bind(( + cfg.threema.host.unwrap_or("localhost".to_owned()), + cfg.threema.port.unwrap_or(443), + ))? + .run(), ); let matrix_server = tokio::spawn(async move { matrix_client.sync(settings).await }); diff --git a/src/matrix/errors.rs b/src/matrix/errors.rs new file mode 100644 index 0000000..f262756 --- /dev/null +++ b/src/matrix/errors.rs @@ -0,0 +1,26 @@ +use thiserror::Error; +use matrix_sdk::{Error}; +use matrix_sdk::ruma::IdParseError; +use crate::errors::StringifyGroupIdError; + +#[derive(Debug, Error)] +pub enum FindMatrixRoomByThreemaGroupIdError { + #[error("No Matrix room for group id found.")] + NoRoomForGroupIdFoundError, +} + +#[derive(Debug, Error)] +pub enum SendToMatrixRoomByThreemaGroupIdError { + #[error("{0}")] + MatrixError(Error), +} + +#[derive(Debug, Error)] +pub enum BindThreemaGroupToMatrixError { + #[error("{0}")] + InvalidGroupId(StringifyGroupIdError), + #[error("{0}")] + MatrixError(Error), + #[error("{0}")] + InvalidMatrixRoomId(IdParseError), +} \ No newline at end of file diff --git a/src/matrix/matrix_client_impl.rs b/src/matrix/matrix_client_impl.rs new file mode 100644 index 0000000..6dd6fb7 --- /dev/null +++ b/src/matrix/matrix_client_impl.rs @@ -0,0 +1,83 @@ +use std::io::Cursor; +use log::{debug, warn}; +use matrix_sdk::Client; +use matrix_sdk::room::Joined; +use matrix_sdk::ruma::events::room::message::RoomMessageEventContent; +use matrix_sdk::ruma::{RoomId, TransactionId}; +use crate::matrix::errors::{BindThreemaGroupToMatrixError, FindMatrixRoomByThreemaGroupIdError, SendToMatrixRoomByThreemaGroupIdError}; +use crate::matrix::MatrixClient; +use crate::matrix::util::{get_threematrix_room_state, set_threematrix_room_state, ThreematrixStateEventContent}; +use crate::threema::util::convert_group_id_to_readable_string; +use async_trait::async_trait; +use matrix_sdk::attachment::AttachmentConfig; + +#[async_trait] +impl MatrixClient for Client { + async fn get_joined_room_by_threema_group_id(&self, threema_group_id: &[u8]) -> Result { + for room in self.joined_rooms() { + match get_threematrix_room_state(&room).await { + Ok(None) => debug!( + "Matrix: Room {:?} does not have proper room state", + &room + .display_name() + .await + .unwrap_or(matrix_sdk::DisplayName::Named("UNKNOWN".to_owned())) + ), + Ok(Some(state)) => { + if let Ok(group_id) = convert_group_id_to_readable_string(&threema_group_id) { + if state.threematrix_threema_group_id == group_id { + return Ok(room); + } + } + } + Err(e) => warn!("Matrix: Could not retrieve room state: {}", e), + } + } + Err(FindMatrixRoomByThreemaGroupIdError::NoRoomForGroupIdFoundError) + } + + async fn send_message_to_matrix_room(&self, room: &Joined, user_name: &str, body: &str, html_body: &str) -> Result<(), SendToMatrixRoomByThreemaGroupIdError> { + let content = RoomMessageEventContent::text_html( + format!("{}: {}", user_name, body).as_str(), + format!("{}: {}", user_name, html_body)); + let txn_id = TransactionId::new(); + room.send(content.clone(), Some(&txn_id)) + .await + .map_err(|e| { + SendToMatrixRoomByThreemaGroupIdError::MatrixError(e) + })?; + return Ok(()); + } + + async fn send_file_to_matrix_room(&self, room: &Joined, body: &str, file: &[u8]) -> Result<(), SendToMatrixRoomByThreemaGroupIdError> { + let mut cursor = Cursor::new(file); + room.send_attachment(body, &mime::IMAGE_JPEG, &mut cursor, AttachmentConfig::new()).await + .map_err(|e| { SendToMatrixRoomByThreemaGroupIdError::MatrixError(e) })?; + + return Ok(()); + } + + async fn bind_threema_group_to_matrix_room(&self, threema_group_id: &[u8], matrix_room_id: &str) -> Result<(), BindThreemaGroupToMatrixError> { + let room_id = <&RoomId>::try_from(matrix_room_id) + .map_err(|e| BindThreemaGroupToMatrixError::InvalidMatrixRoomId(e))?; + + let room = self.get_joined_room(room_id).unwrap(); + + match convert_group_id_to_readable_string(&threema_group_id) { + Ok(r) => { + let content: ThreematrixStateEventContent = ThreematrixStateEventContent { + threematrix_threema_group_id: r, + }; + + if let Err(e) = set_threematrix_room_state(content, &room).await { + return Err(BindThreemaGroupToMatrixError::MatrixError(e)); + } else { + return Ok(()); + }; + } + Err(e) => { + return Err(BindThreemaGroupToMatrixError::InvalidGroupId(e)); + } + } + } +} \ No newline at end of file diff --git a/src/matrix/mod.rs b/src/matrix/mod.rs index 2bcff54..db2f74d 100644 --- a/src/matrix/mod.rs +++ b/src/matrix/mod.rs @@ -1,42 +1,34 @@ pub mod util; +pub mod errors; +pub mod matrix_client_impl; -use log::{debug, error, info}; -use matrix_sdk::{room::Room, ruma::events::room::member::StrippedRoomMemberEvent, Client}; -use tokio::time::{sleep, Duration}; +use async_trait::async_trait; +use matrix_sdk::room::Joined; +use crate::matrix::errors::{BindThreemaGroupToMatrixError, FindMatrixRoomByThreemaGroupIdError, SendToMatrixRoomByThreemaGroupIdError}; -// Source: https://github.com/matrix-org/matrix-rust-sdk/blob/matrix-sdk-0.5.0/crates/matrix-sdk/examples/autojoin.rs -pub async fn on_stripped_state_member( - room_member: StrippedRoomMemberEvent, - client: Client, - room: Room, -) { - if room_member.state_key != client.user_id().await.unwrap() { - return; - } - if let Room::Invited(room) = room { - debug!("Matrix: Autojoining room {}", room.room_id()); - let mut delay = 2; - - while let Err(err) = room.accept_invitation().await { - // retry autojoin due to synapse sending invites, before the - // invited user can join for more information see - // https://github.com/matrix-org/synapse/issues/4345 - error!( - "Matrix: Failed to join room {} ({:?}), retrying in {}s", - room.room_id(), - err, - delay - ); - - sleep(Duration::from_secs(delay)).await; - delay *= 2; - - if delay > 3600 { - error!("Matrix: Can't join room {} ({:?})", room.room_id(), err); - break; - } - } - info!("Matrix: Successfully joined room {}", room.room_id()); - } +#[async_trait] +pub trait MatrixClient { + async fn get_joined_room_by_threema_group_id( + &self, + threema_group_id: &[u8], + ) -> Result; + async fn send_message_to_matrix_room( + &self, + room: &Joined, + user_name: &str, + body: &str, + html_body: &str, + ) -> Result<(), SendToMatrixRoomByThreemaGroupIdError>; + async fn send_file_to_matrix_room( + &self, + room: &Joined, + body: &str, + file: &[u8], + ) -> Result<(), SendToMatrixRoomByThreemaGroupIdError>; + async fn bind_threema_group_to_matrix_room( + &self, + threema_group_id: &[u8], + matrix_room_id: &str, + ) -> Result<(), BindThreemaGroupToMatrixError>; } diff --git a/src/threema/mod.rs b/src/threema/mod.rs index a6785b5..4667c7e 100644 --- a/src/threema/mod.rs +++ b/src/threema/mod.rs @@ -1,17 +1,17 @@ use std::collections::{HashMap, HashSet}; use std::sync::Arc; -use threema_gateway::{ApiBuilder, E2eApi, IncomingMessage, PublicKey}; +use log::{debug, info}; +use matrix_sdk::ruma::exports::serde_json; +use mime::Mime; +use threema_gateway::{ApiBuilder, decrypt_file_data, E2eApi, encrypt_file_data, FileMessage, IncomingMessage, PublicKey, RenderingType}; +use threema_gateway::errors::{ApiBuilderError, ApiError}; use tokio::sync::Mutex; use crate::errors::{ProcessIncomingMessageError, SendGroupMessageError}; -use log::{debug, info}; -use threema_gateway::errors::{ApiBuilderError, ApiError}; +use crate::threema::serialization::{encrypt_group_file_msg, encrypt_group_sync_req_msg}; +use crate::threema::types::{GroupCreateMessage, GroupFileMessage, GroupRenameMessage, GroupTextMessage, MessageBase, MessageType, TextMessage}; -use crate::threema::serialization::encrypt_group_sync_req_msg; -use crate::threema::types::{ - GroupCreateMessage, GroupRenameMessage, GroupTextMessage, MessageBase, MessageType, TextMessage, -}; use crate::util::retry_request; use self::serialization::encrypt_group_text_msg; @@ -30,6 +30,8 @@ pub struct ThreemaClient { pub const GROUP_ID_NUM_BYTES: usize = 8; pub const GROUP_CREATOR_NUM_BYTES: usize = 8; pub const MESSAGE_TYPE_NUM_BYTES: usize = 1; +pub const BLOB_ID_LEN: usize = 16; +pub const BLOB_KEY_LEN: usize = 32; pub const THREEMA_ID_LENGTH: usize = 8; impl ThreemaClient { @@ -64,6 +66,28 @@ impl ThreemaClient { } } + + pub async fn send_group_file_by_group_id( + &self, + file: &[u8], + thumbnail: Option<&[u8]>, + description: Option<&str>, + file_name: &str, + mime: Mime, + group_id: &[u8], + ) -> Result<(), SendGroupMessageError> { + let groups = self.groups.lock().await; + if let Some(group) = groups.get(group_id) { + let receiver: Vec<&str> = group.members.iter().map(|str| str.as_str()).collect(); + return self + .send_group_file(file, thumbnail, description, file_name, mime, &group.group_creator, group_id, receiver.as_slice()) + .await + .map_err(|e| SendGroupMessageError::ApiError(e)); + } else { + return Err(SendGroupMessageError::GroupNotInCache); + } + } + pub async fn send_group_msg( &self, text: &str, @@ -84,7 +108,57 @@ impl ThreemaClient { 20 * 1000, 6, ) - .await?; + .await?; + debug!("Threema: Message sent successfully"); + } + return Ok(()); + } + + pub async fn send_group_file( + &self, + file: &[u8], + thumbnail: Option<&[u8]>, + description: Option<&str>, + file_name: &str, + mime: Mime, + group_creator: &str, + group_id: &[u8], + receivers: &[&str], + ) -> Result<(), ApiError> { + let (encrypted_file, encrypted_thumb, key) = encrypt_file_data(file, thumbnail); + + + let api = self.api.lock().await; + // Upload files to blob server + let file_blob_id = api.blob_upload_raw(&encrypted_file, false).await.unwrap(); + let thumb_blob_id = if let Some(et) = encrypted_thumb { + let blob_id = api.blob_upload_raw(&et, false).await.unwrap(); + Some((blob_id, mime.clone())) + } else { + None + }; + let file_message = FileMessage::builder(file_blob_id, key, mime, file.len() as u32) + .thumbnail_opt(thumb_blob_id) + .file_name_opt(Some(file_name)) + .description_opt(description) + .rendering_type(RenderingType::Media) + .build() + .expect("Could not build FileMessage"); + + + for user_id in receivers { + debug!("Threema: Sending message to: {}", user_id); + let public_key = self.lookup_pubkey_with_retry(user_id, &api).await?; //TODO cache + + let encrypted_msg = + encrypt_group_file_msg(&file_message, group_creator, group_id, &public_key.into(), &api); + + retry_request( + || async { api.send(&user_id, &encrypted_msg, false).await }, + 20 * 1000, + 6, + ) + .await?; debug!("Threema: Message sent successfully"); } return Ok(()); @@ -112,7 +186,7 @@ impl ThreemaClient { 20 * 1000, 6, ) - .await?; + .await?; debug!("Threema: Group sync message sent successfully"); return Ok(()); } @@ -121,11 +195,11 @@ impl ThreemaClient { &self, incoming_message: &IncomingMessage, ) -> Result { - let data; + let pubkey; { let api = self.api.lock().await; - let pubkey = self + pubkey = self .lookup_pubkey_with_retry(&incoming_message.from, &api) .await .map_err(|e| ProcessIncomingMessageError::ApiError(e))?; @@ -157,14 +231,14 @@ impl ThreemaClient { data[MESSAGE_TYPE_NUM_BYTES..MESSAGE_TYPE_NUM_BYTES + GROUP_CREATOR_NUM_BYTES] .to_vec(), ) - .map_err(|e| ProcessIncomingMessageError::Utf8ConvertError(e))?; + .map_err(|e| ProcessIncomingMessageError::Utf8ConvertError(e))?; let group_id = &data[MESSAGE_TYPE_NUM_BYTES + GROUP_CREATOR_NUM_BYTES ..MESSAGE_TYPE_NUM_BYTES + GROUP_CREATOR_NUM_BYTES + GROUP_ID_NUM_BYTES]; let text = String::from_utf8( data[MESSAGE_TYPE_NUM_BYTES + GROUP_CREATOR_NUM_BYTES + GROUP_ID_NUM_BYTES..] .to_vec(), ) - .map_err(|e| ProcessIncomingMessageError::Utf8ConvertError(e))?; + .map_err(|e| ProcessIncomingMessageError::Utf8ConvertError(e))?; // Show result debug!( @@ -189,6 +263,48 @@ impl ThreemaClient { group_id: group_id.to_vec(), })); } + MessageType::GroupFile => { + let mut i = MESSAGE_TYPE_NUM_BYTES; + let group_creator = String::from_utf8(data[i..i + GROUP_CREATOR_NUM_BYTES].to_vec()).unwrap(); + + i = i + GROUP_CREATOR_NUM_BYTES; + let group_id = &data[i..i + GROUP_ID_NUM_BYTES]; + + i = i + GROUP_ID_NUM_BYTES; + let file_data_json = String::from_utf8(data[i..].to_vec()).unwrap(); + let file_metadata = serde_json::from_str::(file_data_json.as_str()).unwrap(); + + // Show result + debug!(" GroupCreator: {}", group_creator); + debug!(" groupId: {:?}", group_id); + debug!(" fileData: {:?}", file_metadata); + + + let file; + { + let api = self.api.lock().await; + let file_encrypted = api.blob_download(file_metadata.file_blob_id()).await.unwrap(); + // let key = hex::decode(file_metadata.blob_encryption_key()).unwrap(); + file = decrypt_file_data(&file_encrypted, file_metadata.blob_encryption_key()).unwrap(); + } + + { + let groups = self.groups.lock().await; + if let None = groups.get(group_id) { + debug!("Threema: Unknown group, sending sync req"); + self.send_group_sync_req_msg(group_id, group_creator.as_str()) + .await + .map_err(|e| ProcessIncomingMessageError::ApiError(e))?; + } + } + return Ok(Message::GroupFileMessage(GroupFileMessage { + base, + file_metadata, + group_creator, + group_id: group_id.to_vec(), + file, + })); + } MessageType::GroupCreate => { let group_id = &data[MESSAGE_TYPE_NUM_BYTES..MESSAGE_TYPE_NUM_BYTES + GROUP_CREATOR_NUM_BYTES]; @@ -199,8 +315,8 @@ impl ThreemaClient { for char in &data[MESSAGE_TYPE_NUM_BYTES + GROUP_CREATOR_NUM_BYTES..] { current_member_id = current_member_id + String::from_utf8(vec![*char]) - .map_err(|e| ProcessIncomingMessageError::Utf8ConvertError(e))? - .as_str(); + .map_err(|e| ProcessIncomingMessageError::Utf8ConvertError(e))? + .as_str(); counter = counter + 1; if counter == THREEMA_ID_LENGTH { members.insert(current_member_id.clone()); @@ -262,7 +378,7 @@ impl ThreemaClient { let group_name = String::from_utf8( data[MESSAGE_TYPE_NUM_BYTES + GROUP_CREATOR_NUM_BYTES..].to_vec(), ) - .map_err(|e| ProcessIncomingMessageError::Utf8ConvertError(e))?; + .map_err(|e| ProcessIncomingMessageError::Utf8ConvertError(e))?; { let mut groups = self.groups.lock().await; @@ -294,4 +410,4 @@ impl ThreemaClient { } } } -} +} \ No newline at end of file diff --git a/src/threema/serialization.rs b/src/threema/serialization.rs index d268c2c..f551af5 100644 --- a/src/threema/serialization.rs +++ b/src/threema/serialization.rs @@ -1,7 +1,4 @@ -use std::iter::repeat; - -use rand::Rng; -use threema_gateway::{E2eApi, EncryptedMessage, RecipientKey}; +use threema_gateway::{E2eApi, EncryptedMessage, FileMessage, RecipientKey}; use crate::threema::types::MessageType; @@ -10,16 +7,7 @@ pub fn encrypt_group_sync_req_msg( recipient_key: &RecipientKey, threema_api: &E2eApi, ) -> EncryptedMessage { - let padding_amount = random_padding_amount(); - let padding = repeat(padding_amount).take(padding_amount as usize); - let msgtype_byte = repeat(MessageType::GroupRequestSync.into()).take(1); - - let padded_plaintext: Vec = msgtype_byte - .chain(group_id.iter().cloned()) - .chain(padding) - .collect(); - - threema_api.encrypt_raw(&padded_plaintext, &recipient_key) + return threema_api.encrypt(group_id, threema_gateway::MessageType::Other(MessageType::GroupRequestSync.into()), &recipient_key); } pub fn encrypt_group_text_msg( @@ -29,10 +17,6 @@ pub fn encrypt_group_text_msg( recipient_key: &RecipientKey, threema_api: &E2eApi, ) -> EncryptedMessage { - let padding_amount = random_padding_amount(); - let padding = repeat(padding_amount).take(padding_amount as usize); - let msgtype_byte = repeat(MessageType::GroupText.into()).take(1); - let data: Vec = group_creator .as_bytes() .iter() @@ -40,15 +24,25 @@ pub fn encrypt_group_text_msg( .chain(group_id.iter().cloned()) .chain(text.as_bytes().iter().cloned()) .collect(); - let padded_plaintext: Vec = msgtype_byte - .chain(data.iter().cloned()) - .chain(padding) - .collect(); - threema_api.encrypt_raw(&padded_plaintext, &recipient_key) + return threema_api.encrypt(data.as_slice(), threema_gateway::MessageType::Other(MessageType::GroupText.into()), &recipient_key); } -fn random_padding_amount() -> u8 { - let mut rng = rand::thread_rng(); - return rng.gen_range(1..255); +pub fn encrypt_group_file_msg( + msg: &FileMessage, + group_creator: &str, + group_id: &[u8], + recipient_key: &RecipientKey, + threema_api: &E2eApi, +) -> EncryptedMessage { + let file_msg_json = serde_json::to_string(msg).unwrap(); + let data: Vec = group_creator + .as_bytes() + .iter() + .cloned() + .chain(group_id.iter().cloned()) + .chain(file_msg_json.as_bytes().iter().cloned()) + .collect(); + + return threema_api.encrypt(data.as_slice(), threema_gateway::MessageType::Other(MessageType::GroupFile.into()), &recipient_key); } diff --git a/src/threema/types.rs b/src/threema/types.rs index 7a73ae7..632b504 100644 --- a/src/threema/types.rs +++ b/src/threema/types.rs @@ -1,3 +1,5 @@ +use threema_gateway::FileMessage; + // Custom internal types #[derive(Debug)] pub struct MessageGroup { @@ -10,6 +12,7 @@ pub struct MessageGroup { pub enum Message { GroupTextMessage(GroupTextMessage), TextMessage(TextMessage), + GroupFileMessage(GroupFileMessage), GroupCreateMessage(GroupCreateMessage), GroupRenameMessage(GroupRenameMessage), } @@ -39,6 +42,14 @@ pub struct GroupTextMessage { pub group_id: Vec, } +pub struct GroupFileMessage { + pub base: MessageBase, + pub file_metadata: FileMessage, + pub group_creator: String, + pub group_id: Vec, + pub file: Vec, +} + #[derive(Clone)] pub struct MessageBase { pub from_identity: String, @@ -48,9 +59,11 @@ pub struct MessageBase { pub date: u64, } + pub enum MessageType { Text, GroupText, + GroupFile, GroupCreate, GroupRename, GroupRequestSync, @@ -65,6 +78,7 @@ impl From for MessageType { match value { 0x01 => MessageType::Text, 0x41 => MessageType::GroupText, + 0x46 => MessageType::GroupFile, 0x4a => MessageType::GroupCreate, 0x4b => MessageType::GroupRename, 0x51 => MessageType::GroupRequestSync, @@ -84,6 +98,7 @@ impl Into for MessageType { match self { MessageType::Text => 0x01, MessageType::GroupText => 0x41, + MessageType::GroupFile => 0x46, MessageType::GroupCreate => 0x4a, MessageType::GroupRename => 0x4b, MessageType::GroupRequestSync => 0x51, @@ -93,4 +108,4 @@ impl Into for MessageType { MessageType::DeliveryReceipt => 0x80, } } -} +} \ No newline at end of file