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
20const INITIAL_DEVICE_DISPLAY_NAME: &str = "login client";
22
23#[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
42async 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 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 LoginType::Token(_) |
65 LoginType::ApplicationService(_) => {},
67 _ => {},
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 client.add_event_handler(on_room_message);
80 client.sync(SyncSettings::new()).await?;
82
83 Ok(())
84}
85
86#[derive(Debug)]
87enum LoginChoice {
88 Password,
90
91 Sso,
93
94 SsoIdp(IdentityProvider),
96}
97
98impl LoginChoice {
99 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
119async 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
150async 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
187async 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 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
210async fn on_room_message(event: OriginalSyncRoomMessageEvent, room: Room) {
212 if room.state() != RoomState::Joined {
214 return;
215 }
216
217 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}