lib.rs
1 use std::{sync::Arc, time::Duration}; 2 3 use anyhow::{Context as _, anyhow, bail}; 4 use matrix_sdk::{ 5 RoomState, 6 authentication::matrix::MatrixSession, 7 ruma::api::client::{ 8 membership::join_room_by_id, 9 uiaa::{self, AuthData, UserIdentifier}, 10 }, 11 store::RoomLoadSettings, 12 }; 13 use reqwest::Url; 14 use tracing::{error, info}; 15 16 use crate::{ 17 aoc::client::AocClient, 18 context::{Context, ContextGarygrady}, 19 matrix::create_client, 20 utils::store::Store, 21 }; 22 23 mod aoc; 24 mod config; 25 mod context; 26 mod mastodon; 27 mod matrix; 28 mod tasks; 29 mod utils; 30 31 const SESSION_STORE_KEY: &[u8] = b"session"; 32 33 pub async fn setup(config_path: impl Iterator<Item = &str>) -> anyhow::Result<()> { 34 let config = config::load(config_path).context("Failed to load config")?; 35 36 let client = create_client(&config.matrix.homeserver, &config.matrix.store_path).await?; 37 38 let store = Store::new(client.clone()); 39 40 // don't overwrite an existing session 41 if store 42 .get::<MatrixSession>(SESSION_STORE_KEY) 43 .await? 44 .is_some() 45 { 46 bail!("The store already contains a session."); 47 } 48 49 // Matrix login 50 eprintln!("Enter either a login token or a username and password for the bot account:"); 51 let line = read_line()?; 52 let mut parts = line.split_whitespace(); 53 let (first, second, third) = (parts.next(), parts.next(), parts.next()); 54 let login = match (first, second, third) { 55 (Some(login_token), None, _) => client 56 .matrix_auth() 57 .login_token(login_token.rsplit('=').next().unwrap_or_default()), 58 (Some(username), Some(password), None) => { 59 client.matrix_auth().login_username(username, password) 60 } 61 _ => bail!("Failed to read credentials from stdin"), 62 }; 63 let response = login.initial_device_display_name("Bot").await?; 64 info!(user_id = ?response.user_id, devicd_id = ?response.device_id, "Login successful"); 65 66 // save session 67 let session = MatrixSession::from(&response); 68 store 69 .set::<MatrixSession>(SESSION_STORE_KEY, &session) 70 .await?; 71 72 // create and upload cross signing keys 73 info!("Trying to bootstrap cross signing"); 74 if let Err(err) = client.encryption().bootstrap_cross_signing(None).await { 75 let Some(response) = err.as_uiaa_response() else { 76 return Err(err.into()); 77 }; 78 let session = response 79 .session 80 .as_ref() 81 .ok_or_else(|| anyhow!("no uiaa session"))? 82 .to_owned(); 83 let auth = if let (Some(username), Some(password)) = (first, second) { 84 let mut password = uiaa::Password::new( 85 UserIdentifier::UserIdOrLocalpart(username.into()), 86 password.into(), 87 ); 88 password.session = Some(session); 89 AuthData::Password(password) 90 } else { 91 let sso_url = client.homeserver().join(&format!( 92 "_matrix/client/v3/auth/m.login.sso/fallback/web?session={session}", 93 ))?; 94 eprintln!("Complete uiaa via sso at {sso_url}, then press enter"); 95 read_line()?; 96 AuthData::fallback_acknowledgement(session) 97 }; 98 client 99 .encryption() 100 .bootstrap_cross_signing(Some(auth)) 101 .await?; 102 } 103 104 // enable backups for encryption keys 105 client.encryption().backups().create().await?; 106 107 // join the matrix room 108 let room_id = config.matrix.room_id; 109 info!("Trying to join room {room_id}"); 110 while let Err(err) = client 111 .send(join_room_by_id::v3::Request::new(room_id.clone())) 112 .await 113 { 114 error!("Failed to join room {room_id}: {err}"); 115 eprintln!("Press enter to try again"); 116 read_line()?; 117 } 118 119 Ok(()) 120 } 121 122 pub async fn run(config_path: impl Iterator<Item = &str>) -> anyhow::Result<()> { 123 let config = config::load(config_path).context("Failed to load config")?; 124 125 let client = create_client(&config.matrix.homeserver, &config.matrix.store_path).await?; 126 127 let store = Store::new(client.clone()); 128 129 // Matrix Login 130 let session = store 131 .get::<MatrixSession>(SESSION_STORE_KEY) 132 .await? 133 .ok_or_else(|| anyhow!("Create a session first using `aocbot setup`"))?; 134 client 135 .matrix_auth() 136 .restore_session(session, RoomLoadSettings::default()) 137 .await?; 138 139 let response = client.whoami().await?; 140 info!(user_id = %response.user_id, devicd_id = %response.device_id.unwrap(), "Matrix login successful"); 141 142 // Advent of Code Login 143 let aoc_session = std::fs::read_to_string(&config.aoc.session_file)?; 144 let aoc_client = AocClient::new( 145 aoc_session.trim(), 146 Duration::from_secs(config.aoc.default_cache_ttl), 147 config 148 .aoc 149 .cache_ttl_rules 150 .iter() 151 .map(|r| (r.minutes_after_unlock, Duration::from_secs(r.ttl))) 152 .collect(), 153 store.clone(), 154 ) 155 .await?; 156 info!( 157 user_id = aoc_client.whoami().user_id, 158 invite_code = aoc_client.whoami().invite_code, 159 "AoC login successful" 160 ); 161 162 // Mastodon setup 163 let garygrady_server = Url::parse("https://mastodon.social/")?; 164 let garygrady_id = mastodon::lookup_account(&garygrady_server, "garygrady") 165 .await? 166 .id; 167 let garygrady = ContextGarygrady { 168 server: garygrady_server, 169 user_id: garygrady_id, 170 }; 171 172 // Setup matrix bot 173 let bot = matrix::Bot::setup(client.clone()).await?; 174 175 // Find matrix room 176 let room_id = &config.matrix.room_id; 177 let room = client 178 .get_room(room_id) 179 .ok_or_else(|| anyhow!("Failed to find matrix room '{room_id}'"))?; 180 if room.state() != RoomState::Joined { 181 info!("Trying to join room {}", room.room_id()); 182 room.join().await?; 183 } 184 185 let context = Arc::new(Context::new(config, store, room, aoc_client, garygrady)); 186 187 tasks::start(Arc::clone(&context)); 188 189 bot.start(context).await 190 } 191 192 fn read_line() -> anyhow::Result<String> { 193 let mut line = String::new(); 194 std::io::stdin().read_line(&mut line)?; 195 Ok(line) 196 }