main.rs (9307B)
1 use std::time::Duration; 2 3 use clap::Parser; 4 use matrix_sdk::{ 5 config::SyncSettings, 6 event_handler::Ctx, 7 matrix_auth::{Session, SessionTokens}, 8 ruma::{ 9 events::room::{ 10 member::StrippedRoomMemberEvent, 11 message::{MessageType, OriginalSyncRoomMessageEvent, RoomMessageEventContent}, 12 }, 13 OwnedUserId, 14 }, 15 Client, Room, RoomState, SessionMeta, 16 }; 17 use secrecy::{ExposeSecret, SecretString}; 18 use sip::SIPRequest; 19 use tokio::time::sleep; 20 use tracing::{error, info, warn}; 21 use uuid::Uuid; 22 23 use crate::{sip::send_invite, webrtc::start_webrtc_call_to_sip}; 24 25 mod db; 26 mod sip; 27 mod webrtc; 28 29 #[derive(Parser)] 30 #[command(author,version, about, long_about= None)] 31 struct Cli { 32 homeserver_url: String, 33 username: String, 34 password: SecretString, 35 sip_server_domain: String, 36 sip_server_address: String, 37 sip_username: String, 38 sip_password: SecretString, 39 } 40 41 #[derive(Debug, Clone)] 42 pub struct SipAuthInfo { 43 pub address: String, 44 pub username: String, 45 pub password: SecretString, 46 } 47 48 #[derive(Debug, Clone)] 49 pub struct CallID(pub String); 50 51 #[tokio::main] 52 async fn main() -> color_eyre::Result<()> { 53 color_eyre::install()?; 54 tracing_subscriber::fmt::init(); 55 let cli = Cli::parse(); 56 57 let (matrix_side_ws_sender, ws_side_matrix_receiver) = 58 tokio::sync::mpsc::channel::<SIPRequest>(4096); 59 60 // TODO: This probably has to be per call instead 61 let call_id = CallID(Uuid::new_v4().to_string()); 62 let call_id_clone = call_id.clone(); 63 let auth_info = SipAuthInfo { 64 address: cli.sip_server_address.clone(), 65 username: cli.sip_username.clone(), 66 password: cli.sip_password.clone(), 67 }; 68 tokio::spawn(async move { 69 // TODO: Handle errors 70 sip::handle_sip_connection( 71 ws_side_matrix_receiver, 72 call_id_clone, 73 cli.sip_server_domain, 74 cli.sip_server_address, 75 cli.sip_username, 76 cli.sip_password, 77 ) 78 .await 79 .unwrap(); 80 }); 81 82 login_and_sync( 83 cli.homeserver_url, 84 &cli.username, 85 cli.password, 86 call_id.clone(), 87 matrix_side_ws_sender, 88 auth_info, 89 ) 90 .await?; 91 92 Ok(()) 93 } 94 95 async fn login_and_sync( 96 homeserver_url: String, 97 username: &str, 98 password: SecretString, 99 call_id: CallID, 100 ws_sender: tokio::sync::mpsc::Sender<SIPRequest>, 101 auth_info: SipAuthInfo, 102 ) -> color_eyre::Result<()> { 103 // Note that when encryption is enabled, you should use a persistent store to be 104 // able to restore the session with a working encryption setup. 105 // See the `persist_session` example. 106 let client = Client::builder() 107 .homeserver_url(homeserver_url) 108 .sqlite_store("./store", None) 109 .build() 110 .await?; 111 112 let db = db::setup_db().await?; 113 if let Some((token, device_id)) = db.get_user(username.to_string()).await? { 114 client 115 .matrix_auth() 116 .restore_session(Session { 117 meta: SessionMeta { 118 user_id: OwnedUserId::try_from(username).unwrap(), 119 device_id: device_id.into(), 120 }, 121 tokens: SessionTokens { 122 access_token: token, 123 refresh_token: None, 124 }, 125 }) 126 .await?; 127 } else { 128 client 129 .matrix_auth() 130 .login_username(username, password.expose_secret()) 131 .initial_device_display_name("autojoin bot") 132 .await?; 133 db.set_user( 134 client.user_id().unwrap().to_string(), 135 client.access_token().unwrap().to_string(), 136 client.device_id().unwrap().to_string(), 137 ) 138 .await?; 139 } 140 141 println!("logged in as {username}"); 142 143 client.add_event_handler_context(ws_sender.clone()); 144 client.add_event_handler_context(call_id.clone()); 145 client.add_event_handler_context(auth_info.clone()); 146 client.add_event_handler(on_stripped_state_member); 147 client.add_event_handler(on_room_message); 148 149 info!("Syncing..."); 150 client.sync(SyncSettings::default()).await?; 151 152 warn!("Syncing crashed"); 153 Ok(()) 154 } 155 156 // This handles autojoining rooms that the bot is invited to. 157 async fn on_stripped_state_member( 158 room_member: StrippedRoomMemberEvent, 159 client: Client, 160 room: Room, 161 ) { 162 if room_member.state_key != client.user_id().unwrap() { 163 return; 164 } 165 166 tokio::spawn(async move { 167 info!("Autojoining room {}", room.room_id()); 168 let mut delay = 2; 169 170 while let Err(err) = room.join().await { 171 // retry autojoin due to synapse sending invites, before the 172 // invited user can join for more information see 173 // https://github.com/matrix-org/synapse/issues/4345 174 error!( 175 "Failed to join room {} ({err:?}), retrying in {delay}s", 176 room.room_id() 177 ); 178 179 sleep(Duration::from_secs(delay)).await; 180 delay *= 2; 181 182 if delay > 3600 { 183 error!("Can't join room {} ({err:?})", room.room_id()); 184 break; 185 } 186 } 187 info!("Successfully joined room {}", room.room_id()); 188 }); 189 } 190 191 // Handle commands for dial-out, dial-in and help 192 async fn on_room_message( 193 event: OriginalSyncRoomMessageEvent, 194 room: Room, 195 ws_sender: Ctx<tokio::sync::mpsc::Sender<SIPRequest>>, 196 call_id: Ctx<CallID>, 197 auth_info: Ctx<SipAuthInfo>, 198 ) { 199 if room.state() != RoomState::Joined { 200 return; 201 } 202 let MessageType::Text(text_content) = event.content.msgtype else { 203 return; 204 }; 205 206 info!("Received message: {}", text_content.body); 207 208 // Handle the Help command 209 if text_content.body.starts_with("!help") { 210 let help_message = RoomMessageEventContent::text_html("Commands:\n\ 211 !dial-out <sip_address> - Dial a SIP address\n\ 212 !dial-in - Allow dial-in via a code and password over SIP into the current conference (starts a new one if needed)\n\ 213 !help - Show this help message", 214 //Version using html lists 215 "<ul>\ 216 <li><code>!dial-out <sip_address></code> - Dial a SIP address</li>\ 217 <li><code>!dial-in</code> - Allow dial-in via a code and password over SIP into the current conference (starts a new one if needed)</li>\ 218 <li><code>!help</code> - Show this help message</li>\ 219 </ul>" 220 ); 221 222 room.send(help_message, None).await.unwrap(); 223 } else if text_content.body.starts_with("!dial-out") { 224 // Handle the dial-out command 225 let sip_address = text_content.body.split(' ').nth(1).unwrap(); 226 227 // Check if sip address starts with `sip:`, or `sips:` or is a number (without `+`) 228 // If it starts with `sip:` or `sips:` we check if it has a valid domain name after the `@` or an IP address 229 // Since we don't need major validation, we just check if it has a `@` and a `.`. 230 // For numbers there are no additional checks 231 if !((sip_address.starts_with("sip:") || sip_address.starts_with("sips:")) 232 && sip_address.contains('@') 233 && sip_address.contains('.')) 234 && !sip_address.chars().all(char::is_numeric) 235 { 236 let call_message = RoomMessageEventContent::notice_plain(format!( 237 "Invalid SIP address: {}. Please use a SIP address starting with `sip:` or `sips:` or a number (without `+`)", 238 sip_address 239 )); 240 241 room.send(call_message, None).await.unwrap(); 242 return; 243 } 244 245 let call_message = 246 RoomMessageEventContent::notice_plain(format!("Starting to dial {}", sip_address)); 247 248 room.send(call_message, None).await.unwrap(); 249 250 let peer = start_webrtc_call_to_sip(room.clone()).await.unwrap(); 251 if let Err(e) = send_invite( 252 sip_address.to_owned(), 253 ws_sender.0, 254 call_id.0.clone(), 255 peer.clone(), 256 room.clone(), 257 auth_info.0, 258 ) 259 .await 260 { 261 error!("Error sending invite: {}", e); 262 let call_message: RoomMessageEventContent = 263 RoomMessageEventContent::notice_plain(format!( 264 "🚨Failed to establish call to {}. Aborting attempts🚨", 265 sip_address 266 )); 267 268 room.send(call_message, None).await.unwrap(); 269 let _ = peer.close().await; 270 return; 271 } 272 } else if text_content.body.starts_with("!dial-in") { 273 // Handle the dial-in command 274 //let call_id = dial_in().await; 275 276 let call_message = RoomMessageEventContent::notice_plain(format!( 277 "Dialing in enabled via sip address: {}\n\ 278 Code: {}\n\ 279 Password: {}", 280 // TODO: Properly generate these 281 "sip:example.com", 282 1234, 283 "password" 284 )); 285 286 room.send(call_message, None).await.unwrap(); 287 } 288 }