sip-bridge-mx

A WIP Bridge between SIP over Websocket+Webrtc and MatrixRTC (Webrtc via Matrix signaling)
git clone git://archive.git.mtrnord.blog/MTRNord/sip-bridge-mx.git
Log | Files | Refs | README | LICENSE

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 &lt;sip_address&gt;</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 }