1use std::io::Write;
2
3use anyhow::{Context, Result, bail};
4use clap::Parser;
5use futures_util::StreamExt;
6use matrix_sdk::{
7 Client,
8 authentication::oauth::{
9 qrcode::{LoginProgress, QrCodeData, QrCodeModeData},
10 registration::{ApplicationType, ClientMetadata, Localized, OAuthGrantType},
11 },
12 ruma::serde::Raw,
13};
14use url::Url;
15
16#[derive(Parser, Debug)]
21struct Cli {
22 #[clap(short, long)]
24 proxy: Option<Url>,
25
26 #[clap(short, long, action)]
28 verbose: bool,
29}
30
31fn client_metadata() -> Raw<ClientMetadata> {
33 let client_uri = Localized::new(
34 Url::parse("https://github.com/matrix-org/matrix-rust-sdk")
35 .expect("Couldn't parse client URI"),
36 None,
37 );
38
39 let metadata = ClientMetadata {
40 client_name: Some(Localized::new("matrix-rust-sdk-qrlogin".to_owned(), [])),
45 policy_uri: Some(client_uri.clone()),
46 tos_uri: Some(client_uri.clone()),
47 ..ClientMetadata::new(
48 ApplicationType::Native,
51 vec![OAuthGrantType::DeviceCode],
53 client_uri,
54 )
55 };
56
57 Raw::new(&metadata).expect("Couldn't serialize client metadata")
58}
59
60async fn print_devices(client: &Client) -> Result<()> {
61 let user_id = client.user_id().unwrap();
62 let own_device =
63 client.encryption().get_own_device().await?.expect("We should have our own device by now");
64
65 println!(
66 "Status of our own device {}",
67 if own_device.is_cross_signed_by_owner() { "✅" } else { "❌" }
68 );
69
70 println!("Devices of user {user_id}");
71
72 for device in client.encryption().get_user_devices(user_id).await?.devices() {
73 if device.device_id()
74 == client.device_id().expect("We should be logged in now and know our device id")
75 {
76 continue;
77 }
78
79 println!(
80 " {:<10} {:<30} {:<}",
81 device.device_id(),
82 device.display_name().unwrap_or("-"),
83 if device.is_verified() { "✅" } else { "❌" }
84 );
85 }
86
87 Ok(())
88}
89
90async fn login(proxy: Option<Url>) -> Result<()> {
91 println!("Please scan the QR code and convert the data to base64 before entering it here.");
92 println!("On Linux/Wayland, this can be achieved using the following command line:");
93 println!(
94 " $ grim -g \"$(slurp)\" - | zbarimg --oneshot -Sbinary PNG:- | base64 -w 0 | wl-copy"
95 );
96 println!("Paste the QR code data here: ");
97
98 let mut input = String::new();
99 std::io::stdin().read_line(&mut input).expect("error: unable to read user input");
100 let input = input.trim();
101
102 let data = QrCodeData::from_base64(input).context("Couldn't parse the base64 QR code data")?;
103
104 let QrCodeModeData::Reciprocate { server_name } = &data.mode_data else {
105 bail!("The QR code is invalid, we did not receive a homeserver in the QR code.");
106 };
107 let mut client = Client::builder().server_name_or_homeserver_url(server_name);
108
109 if let Some(proxy) = proxy {
110 client = client.proxy(proxy).disable_ssl_verification();
111 }
112
113 let client = client.build().await?;
114
115 let registration_data = client_metadata().into();
116 let oauth = client.oauth();
117
118 let login_client = oauth.login_with_qr_code(&data, Some(®istration_data));
119 let mut subscriber = login_client.subscribe_to_progress();
120
121 let task = tokio::spawn(async move {
122 while let Some(state) = subscriber.next().await {
123 match state {
124 LoginProgress::Starting => (),
125 LoginProgress::EstablishingSecureChannel { check_code } => {
126 let code = check_code.to_digit();
127 println!("Please enter the following code into the other device {code:02}");
128 }
129 LoginProgress::WaitingForToken { user_code } => {
130 println!("Please use your other device to confirm the log in {user_code}")
131 }
132 LoginProgress::Done => break,
133 }
134 }
135
136 std::io::stdout().flush().expect("Unable to write to stdout");
137 });
138
139 let result = login_client.await;
140 task.abort();
141
142 result?;
143
144 let status = client.encryption().cross_signing_status().await.unwrap();
145 let user_id = client.user_id().unwrap();
146
147 println!(
148 "Successfully logged in as {user_id} using the qr code, cross-signing status: {status:?}"
149 );
150
151 print_devices(&client).await?;
152
153 Ok(())
154}
155
156#[tokio::main]
157async fn main() -> Result<()> {
158 let cli = Cli::parse();
159
160 if cli.verbose {
161 tracing_subscriber::fmt::init();
162 }
163
164 login(cli.proxy).await?;
165
166 Ok(())
167}