Skip to main content

example_login/
main.rs

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