Compare commits
5 Commits
Author | SHA1 | Date |
---|---|---|
Rob Watson | 0fe422f609 | |
Rob Watson | bc2e1d0f40 | |
Rob Watson | 68200ee28a | |
Rob Watson | 7e5eb098e3 | |
Rob Watson | bcdc7c7e01 |
|
@ -1 +1,2 @@
|
|||
/target
|
||||
.env
|
||||
|
|
File diff suppressed because it is too large
Load Diff
10
Cargo.toml
10
Cargo.toml
|
@ -4,6 +4,12 @@ version = "0.1.0"
|
|||
authors = ["Rob Watson <rfwatson@users.noreply.github.com>"]
|
||||
edition = "2018"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
chrono = "0.4"
|
||||
dotenv = "0.15.0"
|
||||
futures-util = "0.3.6"
|
||||
reqwest = { version = "0.10.8", features = ["json"] }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
slack_api = "0.23.1"
|
||||
tokio = { version = "0.2", features = ["full"] }
|
||||
|
|
|
@ -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())
|
||||
}
|
141
src/main.rs
141
src/main.rs
|
@ -1,3 +1,140 @@
|
|||
fn main() {
|
||||
println!("Hello, world!");
|
||||
mod config;
|
||||
mod slack;
|
||||
mod weather;
|
||||
|
||||
use chrono::Timelike;
|
||||
use config::ConfigUser;
|
||||
use dotenv::dotenv;
|
||||
use serde::Serialize;
|
||||
use slack::SlackUser;
|
||||
use tokio::prelude::*;
|
||||
use weather::WeatherSummary;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
dotenv().ok();
|
||||
|
||||
let config = config::get_config().unwrap();
|
||||
println!("Got config: {:?}", &config);
|
||||
|
||||
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 offset = chrono::offset::FixedOffset::east(slack_user.tz_offset);
|
||||
let now = chrono::offset::Utc::now().with_timezone(&offset);
|
||||
let is_pm = now.hour() < 6 || now.hour() >= 18;
|
||||
let sum = weather::get_summary(&config_user.location).await?;
|
||||
|
||||
match &sum {
|
||||
WeatherSummary::Thunderstorm(_, _)
|
||||
| WeatherSummary::Clouds(_, _)
|
||||
| WeatherSummary::Snow(_, _)
|
||||
| WeatherSummary::Rain(_, _)
|
||||
| WeatherSummary::Drizzle(_, _)
|
||||
| WeatherSummary::Clear(_, _)
|
||||
| WeatherSummary::Atmospheric(_, _) => {
|
||||
let status = status_text(&sum, is_pm);
|
||||
let emoji = format!(":{}:", &emoji(&sum, is_pm));
|
||||
|
||||
println!(
|
||||
"Set status for user {} to: {} {:?}",
|
||||
&slack_user.id, &emoji, &status
|
||||
);
|
||||
let res = slack::set_status(&slack_user.id, &status, &emoji, 0).await?;
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn status_text(sum: &WeatherSummary, is_pm: bool) -> String {
|
||||
match sum {
|
||||
WeatherSummary::Thunderstorm(temp_min, temp_max) => {
|
||||
if is_pm {
|
||||
format!("Thunderstorm, min {}\u{00b0}C", temp_min)
|
||||
} else {
|
||||
format!("Thunderstorm, max {}\u{00b0}C", temp_max)
|
||||
}
|
||||
}
|
||||
WeatherSummary::Clouds(temp_min, temp_max) => {
|
||||
if is_pm {
|
||||
format!("Cloudy, min {}\u{00b0}C", temp_min)
|
||||
} else {
|
||||
format!("Cloudy, max {}\u{00b0}C", temp_max)
|
||||
}
|
||||
}
|
||||
WeatherSummary::Snow(temp_min, temp_max) => {
|
||||
if is_pm {
|
||||
format!("Snow, min {}\u{00b0}C", temp_min)
|
||||
} else {
|
||||
format!("Snow, max {}\u{00b0}C", temp_max)
|
||||
}
|
||||
}
|
||||
WeatherSummary::Rain(temp_min, temp_max) => {
|
||||
if is_pm {
|
||||
format!("Rain, min {}\u{00b0}C", temp_min)
|
||||
} else {
|
||||
format!("Rain, max {}\u{00b0}C", temp_max)
|
||||
}
|
||||
}
|
||||
WeatherSummary::Drizzle(temp_min, temp_max) => {
|
||||
if is_pm {
|
||||
format!("Drizzle, min {}\u{00b0}C", temp_min)
|
||||
} else {
|
||||
format!("Drizzle, max {}\u{00b0}C", temp_max)
|
||||
}
|
||||
}
|
||||
WeatherSummary::Clear(temp_min, temp_max) => {
|
||||
if is_pm {
|
||||
format!("Clear, min {}\u{00b0}C", temp_min)
|
||||
} else {
|
||||
format!("Sunny, max {}\u{00b0}C", temp_max)
|
||||
}
|
||||
}
|
||||
WeatherSummary::Atmospheric(temp_min, temp_max) => {
|
||||
if is_pm {
|
||||
format!("Foggy, min {}\u{00b0}C", temp_min)
|
||||
} else {
|
||||
format!("Foggy, max {}\u{00b0}C", temp_max)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn emoji(sum: &WeatherSummary, is_pm: bool) -> String {
|
||||
let emoji = match sum {
|
||||
WeatherSummary::Thunderstorm(_, _) => "thunder_cloud_and_rain",
|
||||
WeatherSummary::Clouds(_, _) => "cloud",
|
||||
WeatherSummary::Snow(_, _) => "snow_cloud",
|
||||
WeatherSummary::Rain(_, _) => "rain_cloud",
|
||||
WeatherSummary::Drizzle(_, _) => "rain_cloud",
|
||||
WeatherSummary::Clear(_, _) => {
|
||||
if is_pm {
|
||||
"crescent_moon"
|
||||
} else {
|
||||
"sunny"
|
||||
}
|
||||
}
|
||||
WeatherSummary::Atmospheric(_, _) => "foggy",
|
||||
};
|
||||
|
||||
emoji.to_string()
|
||||
}
|
||||
|
|
|
@ -0,0 +1,83 @@
|
|||
use futures_util::future::TryFutureExt;
|
||||
use serde::Serialize;
|
||||
use serde_json::json;
|
||||
use slack_api;
|
||||
use std::env;
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
struct SlackProfile<'a> {
|
||||
status_text: &'a str,
|
||||
status_emoji: &'a str,
|
||||
status_expiration: i32,
|
||||
}
|
||||
|
||||
#[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 api_key = env::var("SLACK_OAUTH_ACCESS_TOKEN").expect("SLACK_OAUTH_ACCESS_TOKEN");
|
||||
let profile = SlackProfile {
|
||||
status_text,
|
||||
status_emoji,
|
||||
status_expiration,
|
||||
};
|
||||
let encoded_profile = json!(profile).to_string();
|
||||
let set_req = slack_api::users_profile::SetRequest {
|
||||
user: Some(&user_id),
|
||||
profile: Some(&encoded_profile),
|
||||
name: None,
|
||||
value: None,
|
||||
};
|
||||
|
||||
// Returns invalid_user unless the Slack app was installed by a team admin
|
||||
// AND the Slack account is on a paid plan. :cry:
|
||||
slack_api::users_profile::set(&client, &api_key, &set_req)
|
||||
.map_err(|e| e.to_string())
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn list_users() -> Result<Vec<SlackUser>, 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 list_req = slack_api::users::ListRequest::default();
|
||||
|
||||
let resp = slack_api::users::list(&client, &api_key, &list_req)
|
||||
.map_err(|e| e.to_string())
|
||||
.await?;
|
||||
|
||||
let users = resp
|
||||
.members
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.map(|m| {
|
||||
println!("Member: {:?}", m);
|
||||
m.into()
|
||||
})
|
||||
.collect::<Vec<SlackUser>>();
|
||||
|
||||
Ok(users)
|
||||
}
|
|
@ -0,0 +1,79 @@
|
|||
use futures_util::future::TryFutureExt;
|
||||
use serde::Deserialize;
|
||||
use std::env;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct APIResponseWeather {
|
||||
main: String,
|
||||
description: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct APIResponseMain {
|
||||
temp: f32,
|
||||
temp_min: f32,
|
||||
temp_max: f32,
|
||||
feels_like: f32,
|
||||
humidity: u32,
|
||||
pressure: u32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct APIResponse {
|
||||
weather: Vec<APIResponseWeather>,
|
||||
main: APIResponseMain,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum WeatherSummary {
|
||||
Thunderstorm(i32, i32),
|
||||
Clouds(i32, i32),
|
||||
Snow(i32, i32),
|
||||
Rain(i32, i32),
|
||||
Drizzle(i32, i32),
|
||||
Clear(i32, i32),
|
||||
Atmospheric(i32, i32),
|
||||
}
|
||||
|
||||
impl From<APIResponse> for WeatherSummary {
|
||||
fn from(resp: APIResponse) -> Self {
|
||||
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() {
|
||||
"Thunderstorm" => Self::Thunderstorm(temp_min, temp_max),
|
||||
"Clouds" => Self::Clouds(temp_min, temp_max),
|
||||
"Snow" => Self::Snow(temp_min, temp_max),
|
||||
"Rain" => Self::Rain(temp_min, temp_max),
|
||||
"Drizzle" => Self::Drizzle(temp_min, temp_max),
|
||||
"Clear" => Self::Clear(temp_min, temp_max),
|
||||
"Atmosphere" => Self::Atmospheric(temp_min, temp_max),
|
||||
_ => panic!("unrecognized weather type"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_summary(loc: &str) -> Result<WeatherSummary, String> {
|
||||
let url = format!(
|
||||
"https://api.openweathermap.org/data/2.5/weather?q={}&appid={}&units=metric",
|
||||
loc,
|
||||
env::var("OPEN_WEATHERMAP_API_KEY").expect("could not load OPEN_WEATHERMAP_API_KEY")
|
||||
);
|
||||
|
||||
let resp = reqwest::get(&url).map_err(|e| e.to_string()).await?;
|
||||
|
||||
//println!("{}", resp.text().map_err(|e| e.to_string()).await?);
|
||||
|
||||
if !resp.status().is_success() {
|
||||
return Err(format!("Weather API response: {}", resp.status()).into());
|
||||
}
|
||||
|
||||
let api_response = resp
|
||||
.json::<APIResponse>()
|
||||
.map_err(|e| e.to_string())
|
||||
.await?;
|
||||
|
||||
println!("Got API resp: {:?}", api_response);
|
||||
|
||||
Ok(api_response.into())
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
{{#*inline "content"}}
|
||||
<h2>hi</h2>
|
||||
{{/inline}}
|
||||
{{~> layout~}}
|
|
@ -0,0 +1,10 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Weatherstat</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Weatherstat</h1>
|
||||
{{~> content}}
|
||||
</body>
|
||||
</html>
|
Loading…
Reference in New Issue