matrix-google-calendar

Bot to manage meetings, pushrules, matrix status as well as showing daily agenda and allowing to shedule stuff.
git clone git://archive.git.mtrnord.blog/MTRNord/matrix-google-calendar.git
Log | Files | Refs

mod.rs (7887B)


      1 use std::{
      2     fs::{self, OpenOptions},
      3     net::SocketAddr,
      4     path::{Path, PathBuf},
      5 };
      6 
      7 use crate::{
      8     errors::Error,
      9     google_calendar::types::{Calendars, ClientSecrets},
     10 };
     11 use axum::{extract, handler::get, http::StatusCode, Router};
     12 use matrix_sdk::events::{room::message::MessageEventContent, AnyMessageEventContent};
     13 use oauth2::{
     14     basic::{BasicClient, BasicTokenResponse},
     15     reqwest::async_http_client,
     16     AuthUrl, AuthorizationCode, ClientId, ClientSecret, CsrfToken, PkceCodeChallenge, RedirectUrl,
     17     RevocationUrl, Scope, TokenResponse, TokenUrl,
     18 };
     19 use reqwest::{header, ClientBuilder};
     20 use serde::Deserialize;
     21 use tracing::info;
     22 
     23 pub mod types;
     24 
     25 trait LoadTokenResponse {
     26     fn get_from_file(mxid: String) -> Option<BasicTokenResponse>;
     27     fn save_to_file(&self, mxid: String);
     28 }
     29 
     30 impl LoadTokenResponse for BasicTokenResponse {
     31     fn get_from_file(mxid: String) -> Option<BasicTokenResponse> {
     32         let path: PathBuf = format!("./tokens/{}.json", mxid).into();
     33         let file = std::fs::File::open(path);
     34         match file {
     35             Ok(file) => {
     36                 let clientsecrets: Result<Self, serde_json::Error> = serde_json::from_reader(&file);
     37                 match clientsecrets {
     38                     Ok(clientsecrets) => Some(clientsecrets),
     39                     Err(_) => None,
     40                 }
     41             }
     42             Err(_) => None,
     43         }
     44     }
     45 
     46     fn save_to_file(&self, mxid: String) {
     47         use std::io::Write;
     48 
     49         let mut path = Path::new("./tokens/").to_path_buf();
     50         if !path.exists() {
     51             fs::create_dir_all(&path).unwrap();
     52         }
     53         path.push(format!("{}.json", mxid));
     54         let mut file = OpenOptions::new()
     55             .write(true)
     56             .create(true)
     57             .open(path)
     58             .unwrap();
     59         let j = serde_json::to_string(self).unwrap();
     60         file.write_all(j.as_bytes()).unwrap();
     61     }
     62 }
     63 
     64 pub async fn login(mxid: String, tx: mrsbfh::Sender) -> Result<(), Error> {
     65     let loaded = BasicTokenResponse::get_from_file(mxid.clone());
     66     if let Some(token) = loaded {
     67         crate::GOOGLE_SESSIONS
     68             .write()
     69             .await
     70             .insert(mxid.clone(), token);
     71     };
     72 
     73     let reader = crate::GOOGLE_SESSIONS.read().await;
     74     if let Some(token) = reader.get(&mxid) {
     75         if let Some(expiry) = token.expires_in() {
     76             if expiry.as_secs() != 0 {
     77                 return Ok(());
     78             } else {
     79                 // TODO do refresh flow
     80                 let _lalalala = "";
     81                 return Ok(());
     82             }
     83         }
     84     }
     85 
     86     // Create an OAuth2 client by specifying the client ID, client secret, authorization URL and
     87     // token URL.
     88     if let Some(secrets) = ClientSecrets::load_clientsecrets() {
     89         let redirect_uri = if let Some(uri) = secrets.web.redirect_uris.first() {
     90             uri.clone()
     91         } else {
     92             String::from("http://localhost")
     93         };
     94         let client = BasicClient::new(
     95             ClientId::new(secrets.web.client_id.clone()),
     96             Some(ClientSecret::new(secrets.web.client_secret.clone())),
     97             AuthUrl::new(secrets.web.auth_uri.clone())?,
     98             Some(TokenUrl::new(secrets.web.token_uri.clone())?),
     99         )
    100         // Google supports OAuth 2.0 Token Revocation (RFC-7009)
    101         .set_revocation_uri(
    102             RevocationUrl::new("https://oauth2.googleapis.com/revoke".to_string())
    103                 .expect("Invalid revocation endpoint URL"),
    104         )
    105         // Set the URL the user will be redirected to after the authorization process.
    106         .set_redirect_uri(RedirectUrl::new(redirect_uri)?);
    107 
    108         // Generate a PKCE challenge.
    109         let (pkce_challenge, pkce_verifier) = PkceCodeChallenge::new_random_sha256();
    110 
    111         // Generate the full authorization URL.
    112         let (auth_url, csrf_token) = client
    113             .authorize_url(CsrfToken::new_random)
    114             // Set the desired scopes.
    115             .add_scope(Scope::new(
    116                 "https://www.googleapis.com/auth/calendar.readonly".to_string(),
    117             ))
    118             .add_scope(Scope::new(
    119                 "https://www.googleapis.com/auth/calendar".to_string(),
    120             ))
    121             .add_scope(Scope::new(
    122                 "https://www.googleapis.com/auth/calendar.events".to_string(),
    123             ))
    124             .add_scope(Scope::new(
    125                 "https://www.googleapis.com/auth/calendar.events.readonly".to_string(),
    126             ))
    127             .add_scope(Scope::new(
    128                 "https://www.googleapis.com/auth/calendar.settings.readonly".to_string(),
    129             ))
    130             // Set the PKCE code challenge.
    131             .set_pkce_challenge(pkce_challenge)
    132             .url();
    133 
    134         {
    135             crate::STATE_TO_MXID
    136                 .write()
    137                 .await
    138                 .insert(csrf_token.secret().clone(), mxid.clone());
    139             crate::LOGIN_SESSIONS_VERIFIER
    140                 .write()
    141                 .await
    142                 .insert(mxid, (client, pkce_verifier, csrf_token));
    143         };
    144 
    145         let content =
    146             AnyMessageEventContent::RoomMessage(MessageEventContent::notice_plain(format!(
    147                 "Please login using this URL: {}\n\nAfterwards rerun the command!",
    148                 auth_url
    149             )));
    150         tx.send(content).await?;
    151     }
    152 
    153     Ok(())
    154 }
    155 
    156 pub async fn start_webserver() {
    157     let app = Router::new().route("/", get(root));
    158     let addr = SocketAddr::from(([127, 0, 0, 1], 55555));
    159     tracing::debug!("listening on {}", addr);
    160     axum::Server::bind(&addr)
    161         .serve(app.into_make_service())
    162         .await
    163         .unwrap();
    164 }
    165 
    166 async fn root(query_params: extract::Query<GoogleResponse>) -> (StatusCode, &'static str) {
    167     let mut writer = crate::LOGIN_SESSIONS_VERIFIER.write().await;
    168     let reader = crate::STATE_TO_MXID.read().await;
    169     let mxid = reader.get(&query_params.state.clone()).unwrap();
    170     if let Some((client, pkce_verifier, crsftoken)) = writer.remove(mxid) {
    171         let request_state = CsrfToken::new(query_params.state.clone());
    172         let auth_code = AuthorizationCode::new(query_params.code.clone());
    173 
    174         if request_state.secret() == crsftoken.secret() {
    175             let token_result = client
    176                 .exchange_code(auth_code)
    177                 // Set the PKCE code verifier.
    178                 .set_pkce_verifier(pkce_verifier)
    179                 .request_async(async_http_client)
    180                 .await;
    181             if let Ok(token) = token_result {
    182                 info!("{}", token.access_token().secret());
    183                 let token: BasicTokenResponse = token;
    184                 token.save_to_file(mxid.clone());
    185                 crate::GOOGLE_SESSIONS
    186                     .write()
    187                     .await
    188                     .insert(mxid.clone(), token);
    189                 return (StatusCode::OK, "Success");
    190             }
    191         }
    192     }
    193 
    194     (StatusCode::INTERNAL_SERVER_ERROR, "Unable to authenticate")
    195 }
    196 
    197 #[derive(Deserialize)]
    198 struct GoogleResponse {
    199     code: String,
    200     state: String,
    201 }
    202 
    203 pub async fn list_calendars(mxid: String) -> Result<Calendars, crate::errors::Error> {
    204     let reader = crate::GOOGLE_SESSIONS.read().await;
    205     let auth_token = reader.get(&mxid).unwrap();
    206     let access_token = auth_token.access_token();
    207 
    208     info!("Got token");
    209 
    210     let mut headers = header::HeaderMap::new();
    211     let auth_token_val =
    212         header::HeaderValue::from_str(&format!("Bearer {}", access_token.secret())).unwrap();
    213     headers.insert("Authorization", auth_token_val);
    214     let client = ClientBuilder::new()
    215         .default_headers(headers)
    216         .build()
    217         .unwrap();
    218 
    219     info!("Got client");
    220 
    221     let res = client
    222         .get("https://www.googleapis.com/calendar/v3/users/me/calendarList")
    223         .send()
    224         .await?
    225         .json::<Calendars>()
    226         .await?;
    227 
    228     info!("Got response");
    229     Ok(res)
    230 }