/* Copyright © 2026 Anzenlang Licensed under the PolyForm Noncommercial License 1.0.0 https://polyformproject.org/licenses/noncommercial/ SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0 SPDX-AI-Restriction: No training allowed. See NOTICE file. See LICENSE file for complete terms. WARNING: The contents of this file may NOT be used to train AI/LLM models. See NOTICE for legal details. */ use std::io::Write; use futures_util::stream::StreamExt; use matrix_sdk::{ encryption::verification::{ Emoji, SasState, SasVerification, Verification, VerificationRequest, VerificationRequestState, format_emojis, }, ruma::UserId, }; prelude! {} async fn wait_for_confirmation(sas: SasVerification, emoji: [Emoji; 7]) -> Res<()> { println!("\ndo the emojis match: \n{}", format_emojis(emoji)); print!("confirm with `yes` or cancel with `no`: "); std::io::stdout() .flush() .context("failed to flush stdout while asking for confirmation")?; let mut input = String::new(); std::io::stdin() .read_line(&mut input) .context("unable to read user input while asking for confirmation")?; match input.trim().to_lowercase().as_ref() { "yes" | "true" | "ok" => sas.confirm().await.context("SAS confirmation failed"), _ => sas.cancel().await.context("SAS cancellation failed"), } } async fn print_devices(user_id: &UserId, client: &Client) -> Res<()> { let mut acc = String::with_capacity(500); for device in client .encryption() .get_user_devices(user_id) .await? .devices() { if device.device_id() == client .device_id() .context("we should be logged in now and know our device id")? { continue; } acc = format!( "{}\n- {:<10} {:<30} {:<}", acc, device.device_id(), device.display_name().unwrap_or("-"), if device.is_verified() { "✅" } else { "❌" } ); } acc = if acc.is_empty() { " none".into() } else { acc }; info!("Devices of user {user_id}:{acc}"); Ok(()) } async fn sas_verification_handler(client: Client, sas: SasVerification) -> Res<()> { debug!( "starting verification with {} {}", &sas.other_device().user_id(), &sas.other_device().device_id() ); print_devices(sas.other_device().user_id(), &client) .await .context("failed to print devices")?; sas.accept().await?; let mut stream = sas.changes(); while let Some(state) = stream.next().await { match state { SasState::KeysExchanged { emojis, decimals: _, } => { tokio::spawn(wait_for_confirmation( sas.clone(), emojis .context("we only support verifications using emojis")? .emojis, )); } SasState::Done { .. } => { let device = sas.other_device(); debug!( "successfully verified device {} {} {:?}", device.user_id(), device.device_id(), device.local_trust_state() ); print_devices(sas.other_device().user_id(), &client) .await .context("failed to print devices")?; break; } SasState::Cancelled(cancel_info) => { debug!( "verification has been cancelled, reason: {}", cancel_info.reason() ); break; } SasState::Created { .. } | SasState::Started { .. } | SasState::Accepted { .. } | SasState::Confirmed => (), } } Ok(()) } pub async fn request_verification_handler(client: Client, request: VerificationRequest) -> Res<()> { info!( "accepting verification request from {}", request.other_user_id() ); request .accept() .await .context("could not accept verification request")?; let mut stream = request.changes(); while let Some(state) = stream.next().await { match state { VerificationRequestState::Created { .. } | VerificationRequestState::Requested { .. } | VerificationRequestState::Ready { .. } => (), VerificationRequestState::Transitioned { verification } => { // We only support SAS verification. if let Verification::SasV1(s) = verification { tokio::spawn(sas_verification_handler(client, s)); break; } } VerificationRequestState::Done | VerificationRequestState::Cancelled(_) => break, } } Ok(()) }