/ src / lib.rs
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  }