example_login/
main.rs

1use std::{
2    env, fmt,
3    io::{self, Write},
4    process::exit,
5};
6
7use anyhow::anyhow;
8use matrix_sdk::{
9    config::SyncSettings,
10    ruma::{
11        api::client::session::get_login_types::v3::{IdentityProvider, LoginType},
12        events::room::message::{MessageType, OriginalSyncRoomMessageEvent},
13    },
14    Client, Room, RoomState,
15};
16use url::Url;
17
18/// The initial device name when logging in with a device for the first time.
19const INITIAL_DEVICE_DISPLAY_NAME: &str = "login client";
20
21/// A simple program that adapts to the different login methods offered by a
22/// Matrix homeserver.
23///
24/// Homeservers usually offer to login either via password, Single Sign-On (SSO)
25/// or both.
26#[tokio::main]
27async fn main() -> anyhow::Result<()> {
28    tracing_subscriber::fmt::init();
29
30    let Some(homeserver_url) = env::args().nth(1) else {
31        eprintln!("Usage: {} <homeserver_url>", env::args().next().unwrap());
32        exit(1)
33    };
34
35    login_and_sync(homeserver_url).await?;
36
37    Ok(())
38}
39
40/// Log in to the given homeserver and sync.
41async fn login_and_sync(homeserver_url: String) -> anyhow::Result<()> {
42    let homeserver_url = Url::parse(&homeserver_url)?;
43    let client = Client::new(homeserver_url).await?;
44
45    // First, let's figure out what login types are supported by the homeserver.
46    let mut choices = Vec::new();
47    let login_types = client.matrix_auth().get_login_types().await?.flows;
48
49    for login_type in login_types {
50        match login_type {
51            LoginType::Password(_) => {
52                choices.push(LoginChoice::Password)
53            }
54            LoginType::Sso(sso) => {
55                if sso.identity_providers.is_empty() {
56                    choices.push(LoginChoice::Sso)
57                } else {
58                    choices.extend(sso.identity_providers.into_iter().map(LoginChoice::SsoIdp))
59                }
60            }
61            // This is used for SSO, so it's not a separate choice.
62            LoginType::Token(_) |
63            // This is only for application services, ignore it here.
64            LoginType::ApplicationService(_) => {},
65            // We don't support unknown login types.
66            _ => {},
67        }
68    }
69
70    match choices.len() {
71        0 => return Err(anyhow!("Homeserver login types incompatible with this client")),
72        1 => choices[0].login(&client).await?,
73        _ => offer_choices_and_login(&client, choices).await?,
74    }
75
76    // Now that we are logged in, we can sync and listen to new messages.
77    client.add_event_handler(on_room_message);
78    // This will sync until an error happens or the program is killed.
79    client.sync(SyncSettings::new()).await?;
80
81    Ok(())
82}
83
84#[derive(Debug)]
85enum LoginChoice {
86    /// Login with username and password.
87    Password,
88
89    /// Login with SSO.
90    Sso,
91
92    /// Login with a specific SSO identity provider.
93    SsoIdp(IdentityProvider),
94}
95
96impl LoginChoice {
97    /// Login with this login choice.
98    async fn login(&self, client: &Client) -> anyhow::Result<()> {
99        match self {
100            LoginChoice::Password => login_with_password(client).await,
101            LoginChoice::Sso => login_with_sso(client, None).await,
102            LoginChoice::SsoIdp(idp) => login_with_sso(client, Some(idp)).await,
103        }
104    }
105}
106
107impl fmt::Display for LoginChoice {
108    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
109        match self {
110            LoginChoice::Password => write!(f, "Username and password"),
111            LoginChoice::Sso => write!(f, "SSO"),
112            LoginChoice::SsoIdp(idp) => write!(f, "SSO via {}", idp.name),
113        }
114    }
115}
116
117/// Offer the given choices to the user and login with the selected option.
118async fn offer_choices_and_login(client: &Client, choices: Vec<LoginChoice>) -> anyhow::Result<()> {
119    println!("Several options are available to login with this homeserver:\n");
120
121    let choice = loop {
122        for (idx, login_choice) in choices.iter().enumerate() {
123            println!("{idx}) {login_choice}");
124        }
125
126        print!("\nEnter your choice: ");
127        io::stdout().flush().expect("Unable to write to stdout");
128        let mut choice_str = String::new();
129        io::stdin().read_line(&mut choice_str).expect("Unable to read user input");
130
131        match choice_str.trim().parse::<usize>() {
132            Ok(choice) => {
133                if choice >= choices.len() {
134                    eprintln!("This is not a valid choice");
135                } else {
136                    break choice;
137                }
138            }
139            Err(_) => eprintln!("This is not a valid choice. Try again.\n"),
140        };
141    };
142
143    choices[choice].login(client).await?;
144
145    Ok(())
146}
147
148/// Login with a username and password.
149async fn login_with_password(client: &Client) -> anyhow::Result<()> {
150    println!("Logging in with username and password…");
151
152    loop {
153        print!("\nUsername: ");
154        io::stdout().flush().expect("Unable to write to stdout");
155        let mut username = String::new();
156        io::stdin().read_line(&mut username).expect("Unable to read user input");
157        username = username.trim().to_owned();
158
159        print!("Password: ");
160        io::stdout().flush().expect("Unable to write to stdout");
161        let mut password = String::new();
162        io::stdin().read_line(&mut password).expect("Unable to read user input");
163        password = password.trim().to_owned();
164
165        match client
166            .matrix_auth()
167            .login_username(&username, &password)
168            .initial_device_display_name(INITIAL_DEVICE_DISPLAY_NAME)
169            .await
170        {
171            Ok(_) => {
172                println!("Logged in as {username}");
173                break;
174            }
175            Err(error) => {
176                println!("Error logging in: {error}");
177                println!("Please try again\n");
178            }
179        }
180    }
181
182    Ok(())
183}
184
185/// Login with SSO.
186async fn login_with_sso(client: &Client, idp: Option<&IdentityProvider>) -> anyhow::Result<()> {
187    println!("Logging in with SSO…");
188
189    let mut login_builder = client.matrix_auth().login_sso(|url| async move {
190        // Usually we would want to use a library to open the URL in the browser, but
191        // let's keep it simple.
192        println!("\nOpen this URL in your browser: {url}\n");
193        println!("Waiting for login token…");
194        Ok(())
195    });
196
197    if let Some(idp) = idp {
198        login_builder = login_builder.identity_provider_id(&idp.id);
199    }
200
201    login_builder.await?;
202
203    println!("Logged in as {}", client.user_id().unwrap());
204
205    Ok(())
206}
207
208/// Handle room messages by logging them.
209async fn on_room_message(event: OriginalSyncRoomMessageEvent, room: Room) {
210    // We only want to listen to joined rooms.
211    if room.state() != RoomState::Joined {
212        return;
213    }
214
215    // We only want to log text messages.
216    let MessageType::Text(msgtype) = &event.content.msgtype else {
217        return;
218    };
219
220    let member = room
221        .get_member(&event.sender)
222        .await
223        .expect("Couldn't get the room member")
224        .expect("The room member doesn't exist");
225    let name = member.name();
226
227    println!("{name}: {}", msgtype.body);
228}