Further implementation

This commit is contained in:
Rob Watson 2020-10-17 22:04:21 +02:00
parent 68200ee28a
commit bc2e1d0f40
6 changed files with 201 additions and 842 deletions

798
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -6,9 +6,9 @@ edition = "2018"
[dependencies] [dependencies]
dotenv = "0.15.0" dotenv = "0.15.0"
futures-util = "0.3.6"
reqwest = { version = "0.10.8", features = ["json"] } reqwest = { version = "0.10.8", features = ["json"] }
rocket = { git = "https://github.com/SergioBenitez/Rocket", default-features = false }
rocket_contrib = { git = "https://github.com/SergioBenitez/Rocket", default-features = false, features = ["handlebars_templates", "json"] }
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0" serde_json = "1.0"
slack_api = "0.23.1" slack_api = "0.23.1"
tokio = { version = "0.2", features = ["full"] }

22
src/config.rs Normal file
View File

@ -0,0 +1,22 @@
use serde::Deserialize;
use std::error::Error;
use std::fs;
#[derive(Debug, Deserialize)]
pub struct ConfigUser {
pub name: String,
pub location: String,
}
#[derive(Debug, Deserialize)]
pub struct Config {
pub config_users: Vec<ConfigUser>,
}
const CONFIG_FILENAME: &str = "config.json";
pub fn get_config() -> Result<Config, Box<dyn Error>> {
let data = fs::read_to_string(CONFIG_FILENAME)?;
serde_json::from_str(&data).map_err(|e| e.into())
}

View File

@ -1,39 +1,49 @@
#[macro_use] mod config;
extern crate rocket;
use dotenv::dotenv;
use rocket_contrib::templates::Template;
use serde::Serialize;
mod slack; mod slack;
mod weather; mod weather;
#[derive(Serialize)] use config::ConfigUser;
struct Context {} use dotenv::dotenv;
use serde::Serialize;
use slack::SlackUser;
use tokio::prelude::*;
#[get("/weather/<loc>")] #[tokio::main]
async fn get_weather(loc: String) -> Result<String, String> { async fn main() {
let sum = weather::get_summary(&loc).await?;
Ok(format!("The weather in {} is: {:?}", loc, sum))
}
#[get("/slack/<msg>")]
async fn set_status(msg: String) -> Result<String, String> {
slack::set_status(&msg).await?;
Ok("woohoo".to_owned())
}
#[get("/")]
async fn index() -> Template {
let ctx = Context {};
Template::render("index", &ctx)
}
#[rocket::launch]
async fn rocket() -> rocket::Rocket {
dotenv().ok(); dotenv().ok();
rocket::ignite() let config = config::get_config().unwrap();
.attach(Template::fairing()) println!("Got config: {:?}", config);
.mount("/", routes![index, get_weather, set_status])
let slack_users = slack::list_users().await.unwrap();
println!("Got {} users: {:?}", slack_users.len(), slack_users);
for slack_user in slack_users {
let config_user = config
.config_users
.iter()
.find(|config_user| config_user.name == slack_user.name);
if let Some(config_user) = config_user {
match set_user_status(&config_user, &slack_user).await {
Ok(_) => (),
Err(e) => println!(
"Error calling set_user_status for user {}: {}",
config_user.name, e
),
}
}
}
}
async fn set_user_status(config_user: &ConfigUser, slack_user: &SlackUser) -> Result<(), String> {
let sum = weather::get_summary(&config_user.location).await?;
let status = format!("{} {}\u{00b0}C", sum.name, sum.temp_max);
let emoji = format!(":{}:", sum.emoji);
println!(
"Set status for user {} to: {} {:?}",
&slack_user.id, &emoji, &status
);
Ok(slack::set_status(&slack_user.id, &status, &emoji, 0).await?)
} }

View File

@ -1,4 +1,4 @@
use crate::rocket::futures::TryFutureExt; use futures_util::future::TryFutureExt;
use serde::Serialize; use serde::Serialize;
use serde_json::json; use serde_json::json;
use slack_api; use slack_api;
@ -11,17 +11,41 @@ struct SlackProfile<'a> {
status_expiration: i32, status_expiration: i32,
} }
pub async fn set_status(msg: &str) -> Result<(), String> { #[derive(Debug, Serialize)]
pub struct SlackUser {
pub id: String,
pub name: String,
pub tz_label: String,
pub tz_offset: i32,
}
impl From<slack_api::User> for SlackUser {
fn from(user: slack_api::User) -> Self {
SlackUser {
id: user.id.unwrap_or_default(),
name: user.name.unwrap_or_default(),
tz_label: user.tz_label.unwrap_or_default(),
tz_offset: user.tz_offset.unwrap_or_default() as i32,
}
}
}
pub async fn set_status(
user_id: &str,
status_text: &str,
status_emoji: &str,
status_expiration: i32,
) -> Result<(), String> {
let client = slack_api::default_client().map_err(|e| e.to_string())?; let client = slack_api::default_client().map_err(|e| e.to_string())?;
let api_key = env::var("SLACK_OAUTH_ACCESS_TOKEN").expect("SLACK_OAUTH_ACCESS_TOKEN"); let api_key = env::var("SLACK_OAUTH_ACCESS_TOKEN").expect("SLACK_OAUTH_ACCESS_TOKEN");
let profile = SlackProfile { let profile = SlackProfile {
status_text: msg, status_text,
status_emoji: ":mountain:", status_emoji,
status_expiration: 0, status_expiration,
}; };
let encoded_profile = json!(profile).to_string(); let encoded_profile = json!(profile).to_string();
let set_req = slack_api::users_profile::SetRequest { let set_req = slack_api::users_profile::SetRequest {
user: None, user: Some(&user_id),
profile: Some(&encoded_profile), profile: Some(&encoded_profile),
name: None, name: None,
value: None, value: None,
@ -34,16 +58,24 @@ pub async fn set_status(msg: &str) -> Result<(), String> {
Ok(()) Ok(())
} }
pub async fn update_all() -> Result<(), String> { pub async fn list_users() -> Result<Vec<SlackUser>, String> {
let client = slack_api::default_client().map_err(|e| e.to_string())?; let client = slack_api::default_client().map_err(|e| e.to_string())?;
let api_key = env::var("SLACK_OAUTH_ACCESS_TOKEN").expect("SLACK_OAUTH_ACCESS_TOKEN"); let api_key = env::var("SLACK_OAUTH_ACCESS_TOKEN").expect("SLACK_OAUTH_ACCESS_TOKEN");
let list_req = slack_api::users::ListRequest::default(); let list_req = slack_api::users::ListRequest::default();
let resp = slack_api::users::list(&client, &api_key, &list_req) let resp = slack_api::users::list(&client, &api_key, &list_req)
.map_err(|e| e.to_string()) .map_err(|e| e.to_string())
.await?; .await?;
let members = resp.members.unwrap(); let users = resp
println!("Got vec with {} members", members.len()); .members
.unwrap_or_default()
.into_iter()
.map(|m| {
println!("Member: {:?}", m);
m.into()
})
.collect::<Vec<SlackUser>>();
Ok(()) Ok(users)
} }

View File

@ -1,4 +1,4 @@
use crate::rocket::futures::TryFutureExt; use futures_util::future::TryFutureExt;
use serde::Deserialize; use serde::Deserialize;
use std::env; use std::env;
@ -11,6 +11,8 @@ struct APIResponseWeather {
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
struct APIResponseMain { struct APIResponseMain {
temp: f32, temp: f32,
temp_min: f32,
temp_max: f32,
feels_like: f32, feels_like: f32,
humidity: u32, humidity: u32,
pressure: u32, pressure: u32,
@ -23,27 +25,77 @@ struct APIResponse {
} }
#[derive(Debug)] #[derive(Debug)]
pub enum WeatherSummary { pub struct WeatherSummary {
Clear(i32), pub name: String,
Drizzle(i32), pub emoji: String,
Rain(i32), pub temp: i32,
Snow(i32), pub temp_min: i32,
Cloudy(i32), pub temp_max: i32,
Thunderstorm(i32),
Other(i32, String),
} }
impl From<APIResponse> for WeatherSummary { impl From<APIResponse> for WeatherSummary {
fn from(resp: APIResponse) -> Self { fn from(resp: APIResponse) -> Self {
let temp = resp.main.temp as i32; let temp = resp.main.temp as i32;
let temp_min = resp.main.temp_min as i32;
let temp_max = resp.main.temp_max as i32;
match resp.weather[0].main.as_ref() { match resp.weather[0].main.as_ref() {
"Thunderstorm" => Self::Thunderstorm(temp), "Thunderstorm" => Self {
"Drizzle" => Self::Drizzle(temp), name: "Thunderstorm".to_string(),
"Rain" => Self::Rain(temp), emoji: "thunder_cloud_and_rain".to_string(),
"Snow" => Self::Snow(temp), temp,
"Clear" => Self::Clear(temp), temp_min,
"Clouds" => Self::Cloudy(temp), temp_max,
_ => Self::Other(temp, resp.weather[0].description.clone()), },
"Clouds" => Self {
name: "Cloudy".to_string(),
emoji: "cloud".to_string(),
temp,
temp_min,
temp_max,
},
"Snow" => Self {
name: "Snow".to_string(),
emoji: "snow_cloud".to_string(),
temp,
temp_min,
temp_max,
},
"Rain" => Self {
name: "Rain".to_string(),
emoji: "rain_cloud".to_string(),
temp,
temp_min,
temp_max,
},
"Drizzle" => Self {
name: "Drizzle".to_string(),
emoji: "rain_cloud".to_string(),
temp,
temp_min,
temp_max,
},
"Clear" => Self {
name: "Clear".to_string(),
emoji: "sunny".to_string(),
temp,
temp_min,
temp_max,
},
"Atmosphere" => Self {
name: "Atmospheric".to_string(),
emoji: "foggy".to_string(),
temp,
temp_min,
temp_max,
},
_ => Self {
name: "Unknown".to_string(),
emoji: "cloud".to_string(),
temp,
temp_min,
temp_max,
},
} }
} }
} }
@ -55,9 +107,12 @@ pub async fn get_summary(loc: &str) -> Result<WeatherSummary, String> {
env::var("OPEN_WEATHERMAP_API_KEY").expect("could not load OPEN_WEATHERMAP_API_KEY") env::var("OPEN_WEATHERMAP_API_KEY").expect("could not load OPEN_WEATHERMAP_API_KEY")
); );
let body = reqwest::get(&url) let resp = reqwest::get(&url).map_err(|e| e.to_string()).await?;
.map_err(|e| e.to_string()) if !resp.status().is_success() {
.await? return Err(format!("Weather API response: {}", resp.status()).into());
}
let body = resp
.json::<APIResponse>() .json::<APIResponse>()
.map_err(|e| e.to_string()) .map_err(|e| e.to_string())
.await?; .await?;