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
18const INITIAL_DEVICE_DISPLAY_NAME: &str = "login client";
20
21#[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
40async 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 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 LoginType::Token(_) |
63 LoginType::ApplicationService(_) => {},
65 _ => {},
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 client.add_event_handler(on_room_message);
78 client.sync(SyncSettings::new()).await?;
80
81 Ok(())
82}
83
84#[derive(Debug)]
85enum LoginChoice {
86 Password,
88
89 Sso,
91
92 SsoIdp(IdentityProvider),
94}
95
96impl LoginChoice {
97 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
117async 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
148async 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
185async 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 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
208async fn on_room_message(event: OriginalSyncRoomMessageEvent, room: Room) {
210 if room.state() != RoomState::Joined {
212 return;
213 }
214
215 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}