Move over to phorge, take this with me
This commit is contained in:
parent
cc4ea42992
commit
c255e5b2f3
|
|
@ -1,3 +1,4 @@
|
|||
/target
|
||||
.idea/
|
||||
Cargo.lock
|
||||
*.db
|
||||
|
|
|
|||
21
Cargo.toml
21
Cargo.toml
|
|
@ -1,27 +1,30 @@
|
|||
[package]
|
||||
name = "manifold"
|
||||
version = "0.1.0"
|
||||
authors = ["Lucy Bladen <admin@jbladen.uk>"]
|
||||
version = "1.0.0"
|
||||
authors = ["Lucy Bladen <admin@lbladen.uk>"]
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[build-dependencies]
|
||||
built = { version = "0.5.1", features = ["git2", "chrono"] }
|
||||
built = { version = "0.6.0", features = ["git2", "chrono"] }
|
||||
|
||||
[dependencies]
|
||||
built = { version = "0.5.1", features = ["git2", "chrono"] }
|
||||
clap = "3.2.10"
|
||||
built = { version = "0.6.0", features = ["git2", "chrono"] }
|
||||
chrono = "0.4.26"
|
||||
clap = "4.3.4"
|
||||
config = { version = "0.13.1", features = [ "yaml" ] }
|
||||
diesel = { version = "1.4.8", features = ["sqlite", "r2d2", "chrono"] }
|
||||
diesel_migrations = "1.4.0"
|
||||
env_logger = "0.9.0"
|
||||
d20 = "0.1.0"
|
||||
diesel = { version = "2.1.0", features = ["sqlite", "r2d2", "chrono"] }
|
||||
diesel_migrations = "2.1.0"
|
||||
env_logger = "0.10.0"
|
||||
log = "0.4.14"
|
||||
num = "0.4.1"
|
||||
poise = "0.5.5"
|
||||
r2d2 = "0.8.9"
|
||||
rand = "0.8.5"
|
||||
regex = "1.5.4"
|
||||
reqwest = "0.11.9"
|
||||
serde = "1.0.136"
|
||||
serde_json = "1.0.79"
|
||||
serenity = { version = "0.11", features = [ "collector", "unstable_discord_api" ] }
|
||||
tokio = { version = "1.16.1", features = ["sync", "macros", "rt-multi-thread"] }
|
||||
|
|
|
|||
|
|
@ -0,0 +1,9 @@
|
|||
# For documentation on how to configure this file,
|
||||
# see https://diesel.rs/guides/configuring-diesel-cli
|
||||
|
||||
[print_schema]
|
||||
file = "src/schema.rs"
|
||||
custom_type_derives = ["diesel::query_builder::QueryId"]
|
||||
|
||||
[migrations_directory]
|
||||
dir = "migrations"
|
||||
|
|
@ -0,0 +1 @@
|
|||
DROP TABLE "userinfo";
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
CREATE TABLE IF NOT EXISTS "userinfo"
|
||||
(
|
||||
user_id BIGINT PRIMARY KEY NOT NULL,
|
||||
username TEXT NOT NULL,
|
||||
weather_location VARCHAR(36),
|
||||
weather_units VARCHAR(1),
|
||||
timezone VARCHAR(8),
|
||||
last_seen BIGINT
|
||||
);
|
||||
|
|
@ -0,0 +1,98 @@
|
|||
use poise::serenity_prelude::*;
|
||||
|
||||
use crate::{ManifoldContext, ManifoldData};
|
||||
use crate::built_info;
|
||||
use crate::error::{ManifoldError, ManifoldResult};
|
||||
|
||||
#[poise::command(prefix_command, track_edits, slash_command)]
|
||||
async fn help(
|
||||
ctx: ManifoldContext<'_>,
|
||||
#[description = "Help about a specific command"] command: Option<String>
|
||||
) -> ManifoldResult<()> {
|
||||
|
||||
let responses = &ctx.data().responses;
|
||||
|
||||
let config = poise::builtins::HelpConfiguration {
|
||||
extra_text_at_bottom: &*format!("{}", responses.get_response(&"help footer".to_string()).unwrap_or(&"".to_string())),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
poise::builtins::help(ctx, command.as_deref(), config).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[poise::command(slash_command, prefix_command)]
|
||||
async fn ping(ctx: ManifoldContext<'_>,) -> ManifoldResult<()> {
|
||||
let requestor = {
|
||||
let calling_user = ctx.author();
|
||||
calling_user.mention().to_string()
|
||||
};
|
||||
|
||||
ctx.say(format!("{}: Ping? Pong!", requestor)).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[poise::command(slash_command, prefix_command, owners_only)]
|
||||
async fn register_commands(ctx: ManifoldContext<'_>) -> ManifoldResult<()> {
|
||||
poise::builtins::register_application_commands_buttons(ctx).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[poise::command(slash_command, prefix_command, track_edits, aliases("sa"), required_permissions = "MODERATE_MEMBERS")]
|
||||
async fn set_activity(ctx: ManifoldContext<'_>, #[rest] #[description="Who to watch"] target: String) -> ManifoldResult<()> {
|
||||
ctx.serenity_context().set_activity(Activity::watching(&target)).await;
|
||||
|
||||
ctx.say(format!("Okay, I'll start watching {}", target)).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[poise::command(slash_command, prefix_command)]
|
||||
async fn version(ctx: ManifoldContext<'_>) -> ManifoldResult<()> {
|
||||
let git_info: String = built_info::GIT_VERSION.unwrap_or("unknown").to_string();
|
||||
let version_string: String = format!("Version {} built at {} revision {}", built_info::PKG_VERSION, built_info::BUILT_TIME_UTC, git_info);
|
||||
|
||||
ctx.send(|f| f
|
||||
.reply(true)
|
||||
.content(version_string)).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[poise::command(slash_command, prefix_command, owners_only)]
|
||||
async fn get_config(ctx: ManifoldContext<'_>, #[description="Config key"] key: String) -> ManifoldResult<()> {
|
||||
let config = &ctx.data().bot_config;
|
||||
|
||||
let value = match config.get_value(&key) {
|
||||
Ok(v) => v.clone(),
|
||||
Err(_) => "not found, sorry!".to_string()
|
||||
};
|
||||
|
||||
ctx.say(format!("Value for key {} was {}", &key, &value)).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[poise::command(slash_command, prefix_command, owners_only)]
|
||||
async fn dump_config(ctx: ManifoldContext<'_>) -> ManifoldResult<()> {
|
||||
let config = &ctx.data().bot_config;
|
||||
|
||||
ctx.say(format!("Config dump; {:?}", config.config)).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[poise::command(slash_command, prefix_command, owners_only)]
|
||||
async fn get_environment(ctx: ManifoldContext<'_>) -> ManifoldResult<()> {
|
||||
let environment = ctx.data().bot_config.get_environment();
|
||||
|
||||
ctx.say(format!("Currently running under the {} environment", environment)).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn commands() -> [poise::Command<ManifoldData, ManifoldError>; 8] {
|
||||
[help(), ping(), register_commands(), set_activity(), version(), get_config(), dump_config(), get_environment()]
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
mod core;
|
||||
mod weather;
|
||||
|
||||
use crate::ManifoldCommand;
|
||||
|
||||
pub fn collect_commands(injected: Vec<ManifoldCommand>) -> Vec<ManifoldCommand> {
|
||||
core::commands().into_iter()
|
||||
.chain(weather::commands())
|
||||
.chain(injected)
|
||||
.collect()
|
||||
}
|
||||
|
|
@ -0,0 +1,174 @@
|
|||
use poise::serenity_prelude::utils::Colour;
|
||||
use d20::roll_range;
|
||||
use poise::ReplyHandle;
|
||||
use crate::{ManifoldCommand, ManifoldContext};
|
||||
|
||||
use crate::models::weather::{Weather, WeatherForecastRequestResponse};
|
||||
use crate::models::user::UserInfo;
|
||||
use crate::error::ManifoldResult;
|
||||
|
||||
#[poise::command(slash_command, prefix_command, aliases("w"))]
|
||||
async fn weather(ctx: ManifoldContext<'_>, #[rest] #[description="Location to look up weather for"] location: Option<String>) -> ManifoldResult<()> {
|
||||
let my_message = ctx.say("Retrieving weather, be patient").await?;
|
||||
|
||||
let weather_forecast = _get_weather(ctx, &my_message, location, Some(1)).await?;
|
||||
|
||||
let responses = &ctx.data().responses;
|
||||
|
||||
my_message.edit(ctx, |m| {
|
||||
m.content("");
|
||||
m.embed(|e| {
|
||||
|
||||
let card_colour = Colour::from_rgb(roll_range(0, 255).unwrap_or(0) as u8, roll_range(0, 255).unwrap_or(0) as u8, roll_range(0, 255).unwrap_or(0) as u8);
|
||||
|
||||
e.colour(card_colour);
|
||||
e.title(format!("Current weather at {}, {}, {}", weather_forecast.location.name, weather_forecast.location.region, weather_forecast.location.country));
|
||||
e.description(format!("Observations recorded at {}.", weather_forecast.current.last_updated));
|
||||
e.image(format!("https:{}", weather_forecast.current.condition.icon));
|
||||
e.fields(vec![
|
||||
("Temperature (Dewpoint)", format!("{}°C/{}°F ({:.1}°C/{:.1}°F)", weather_forecast.current.temp_c, weather_forecast.current.temp_f, weather_forecast.current.dewpoint_c.unwrap_or(0.0), weather_forecast.current.dewpoint_f.unwrap_or(0.0)), true),
|
||||
("Feels like", format!("{}°C/{}°F", weather_forecast.current.feelslike_c, weather_forecast.current.feelslike_f), true),
|
||||
("Condition", format!("{}", weather_forecast.current.condition.text), true),
|
||||
("Pressure", format!("{}mb/{}in", weather_forecast.current.pressure_mb, weather_forecast.current.pressure_in), true),
|
||||
("Precipitation", format!("{}mm/{}in", weather_forecast.current.precip_mm, weather_forecast.current.precip_in), true),
|
||||
("Humidity", format!("{}%", weather_forecast.current.humidity), true),
|
||||
("Cloud coverage", format!("{}%", weather_forecast.current.cloud), true),
|
||||
("UV index", format!("{}", weather_forecast.current.uv), true),
|
||||
("Coordinates", format!("Lat: {} Lon: {}", weather_forecast.location.lat, weather_forecast.location.lon), true),
|
||||
]);
|
||||
e.field("Wind", format!("{}mph/{}kph from the {} ({} degrees), gusting to {}mph/{}kph", weather_forecast.current.wind_mph, weather_forecast.current.wind_kph, weather_forecast.current.wind_dir, weather_forecast.current.wind_degree, weather_forecast.current.gust_mph, weather_forecast.current.gust_kph), false);
|
||||
e.footer(|f| {
|
||||
f.text(format!("{}", responses.get_response(&"weather card footer".to_string()).unwrap_or(&"Weather Powered By Deez Nutz".to_string())));
|
||||
f
|
||||
});
|
||||
e
|
||||
});
|
||||
m
|
||||
}).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/*
|
||||
#[command]
|
||||
#[aliases("wf")]
|
||||
pub async fn weather_forecast(ctx: &Context, msg: &Message, args: Args) -> CommandResult {
|
||||
|
||||
let mut my_message = msg.reply_ping(&ctx, "Retrieving weather, be patient").await?;
|
||||
|
||||
let weather_forecast = _get_weather(ctx, msg, args, Some(3)).await?;
|
||||
|
||||
let forecast_index: usize = (chrono::NaiveDateTime::parse_from_str(&*weather_forecast.location.localtime, "%Y-%m-%d %H:%M").unwrap().hour() as usize) + 1;
|
||||
|
||||
debug!("{:?}", weather_forecast);
|
||||
|
||||
my_message.edit(&ctx, |m| {
|
||||
m.content("");
|
||||
|
||||
for day in &weather_forecast.forecast.forecastday {
|
||||
m.add_embed(|e| {
|
||||
|
||||
let this_forecast = &day.hour[forecast_index];
|
||||
let card_colour = Colour::from_rgb(roll_range(0, 255).unwrap_or(0) as u8, roll_range(0, 255).unwrap_or(0) as u8, roll_range(0, 255).unwrap_or(0) as u8);
|
||||
|
||||
e.colour(card_colour);
|
||||
e.title(format!("Weather for {} at {}, {}, {}", this_forecast.time, weather_forecast.location.name, weather_forecast.location.region, weather_forecast.location.country));
|
||||
e.description(format!("Observations recorded at {}.", weather_forecast.current.last_updated));
|
||||
e.fields(vec![
|
||||
("Temperature (Dewpoint)", format!("{}°C/{}°F ({:.1}°C/{:.1}°F)", this_forecast.temp_c, this_forecast.temp_f, this_forecast.dewpoint_c, this_forecast.dewpoint_f), true),
|
||||
("Feels like", format!("{}°C/{}°F", this_forecast.feelslike_c, this_forecast.feelslike_f), true),
|
||||
("Condition", format!("{}", this_forecast.condition.text), true),
|
||||
("Pressure", format!("{}mb/{}in", this_forecast.pressure_mb, this_forecast.pressure_in), true),
|
||||
("Precipitation", format!("{}mm/{}in", this_forecast.precip_mm, this_forecast.precip_in), true),
|
||||
("Humidity", format!("{}%", this_forecast.humidity), true),
|
||||
]);
|
||||
e.field("Wind", format!("{}mph/{}kph from the {} ({} degrees), gusting to {}mph/{}kph", this_forecast.wind_mph, this_forecast.wind_kph, this_forecast.wind_dir, this_forecast.wind_degree, this_forecast.gust_mph, this_forecast.gust_kph), false);
|
||||
e
|
||||
});
|
||||
}
|
||||
|
||||
m
|
||||
}).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
*/
|
||||
|
||||
#[poise::command(slash_command, prefix_command, aliases("wl"))]
|
||||
pub async fn save_weather_location(ctx: ManifoldContext<'_>, #[description="Your default weather location"] location: String) -> ManifoldResult<()> {
|
||||
|
||||
let userinfo = &mut ctx.data().user_info.lock().await;
|
||||
let db = &ctx.data().database;
|
||||
|
||||
if let Some(existing_user) = userinfo.get_mut(&ctx.author().id.as_u64()) {
|
||||
existing_user.weather_location = Some(location.clone());
|
||||
existing_user.save(db)?;
|
||||
} else {
|
||||
let new_user = UserInfo {
|
||||
user_id: ctx.author().id.as_u64().clone() as i64,
|
||||
username: ctx.author().name.to_owned(),
|
||||
weather_location: Some(location.clone()),
|
||||
weather_units: None,
|
||||
timezone: None,
|
||||
last_seen: Some(chrono::Utc::now().timestamp()),
|
||||
};
|
||||
new_user.insert(db)?;
|
||||
userinfo.insert(ctx.author().id.as_u64().clone(), new_user);
|
||||
}
|
||||
|
||||
ctx.say(format!("Okay. Now I know that you live in {}. Are you sure that was wise?", &location)).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn _get_weather(ctx: ManifoldContext<'_>, message_handle: &ReplyHandle<'_>, location: Option<String>, days: Option<i32>) -> ManifoldResult<WeatherForecastRequestResponse> {
|
||||
|
||||
let mut weather_location: String = String::default();
|
||||
|
||||
let responses = &ctx.data().responses;
|
||||
let config = &ctx.data().bot_config;
|
||||
|
||||
let userinfo = &mut ctx.data().user_info.lock().await;
|
||||
|
||||
let weather_client = Weather::new(config.get_value(&"WeatherApiKey".to_string()).unwrap().clone(), config.get_value(&"WeatherBaseUrl".to_string()).unwrap().clone());
|
||||
|
||||
if let Some(loc) = location {
|
||||
weather_location = loc;
|
||||
} else {
|
||||
debug!("No arguments provided, looking for stored user");
|
||||
|
||||
if let Some(user) = userinfo.get(&ctx.author().id.as_u64()) {
|
||||
debug!("Have a user reference, checking if they have stored weather info");
|
||||
match &user.weather_location {
|
||||
Some(w) => {
|
||||
weather_location = w.to_owned();
|
||||
},
|
||||
None => {
|
||||
message_handle.edit(ctx, |m| {
|
||||
m.content = responses.get_response(&"weather noloc".to_string()).cloned(); m
|
||||
}).await?;
|
||||
Err("No location provided")?;
|
||||
}
|
||||
};
|
||||
} else {
|
||||
ctx.say(responses.get_response(&"weather noloc".to_string()).unwrap_or(&"B I don't know where you live, and you didn't tell me, so I can't help. Look out of the window.".to_string())).await?;
|
||||
Err("No location provided")?;
|
||||
}
|
||||
}
|
||||
|
||||
debug!("Making weather request!");
|
||||
|
||||
let weather_forecast: WeatherForecastRequestResponse = match weather_client.get_weather_forecast(weather_location, days).await {
|
||||
Ok(w) => w,
|
||||
Err(e) => {
|
||||
ctx.say(format!("Something went wrong. Maybe there is no weather there. {:?}", e)).await?;
|
||||
Err("unknown")?
|
||||
}
|
||||
};
|
||||
|
||||
Ok(weather_forecast)
|
||||
}
|
||||
|
||||
pub fn commands() -> [ManifoldCommand; 2] {
|
||||
[weather(), save_weather_location()]
|
||||
}
|
||||
|
|
@ -1,8 +1,9 @@
|
|||
use std::path::PathBuf;
|
||||
use config::Config;
|
||||
use serenity::model::prelude::{ChannelId, MessageId, RoleId};
|
||||
use poise::serenity_prelude::model::prelude::{ChannelId, MessageId, RoleId};
|
||||
use crate::ManifoldResult;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ManifoldConfig {
|
||||
pub environment: String,
|
||||
pub path: PathBuf,
|
||||
|
|
|
|||
|
|
@ -1,121 +0,0 @@
|
|||
use serenity::{
|
||||
prelude::*,
|
||||
framework::standard::{
|
||||
macros::command,
|
||||
CommandResult,
|
||||
Args,
|
||||
},
|
||||
model::prelude::*
|
||||
};
|
||||
|
||||
use crate::ManifoldConfig;
|
||||
use crate::built_info;
|
||||
|
||||
#[command]
|
||||
async fn ping(ctx: &Context, msg: &Message) -> CommandResult {
|
||||
msg.reply_ping(ctx, "Pong!").await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[command]
|
||||
#[aliases("sa")]
|
||||
async fn set_activity(ctx: &Context, msg: &Message, args: Args) -> CommandResult {
|
||||
|
||||
let activity_name = args.raw().collect::<Vec<&str>>().join(" ");
|
||||
|
||||
msg.reply_ping(&ctx, "OK: I'm going to start playing that.").await.unwrap();
|
||||
|
||||
ctx.set_activity(Activity::playing(&activity_name)).await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[command]
|
||||
#[aliases("v")]
|
||||
async fn version(ctx: &Context, msg: &Message) -> CommandResult {
|
||||
|
||||
let git_info: String = built_info::GIT_VERSION.unwrap_or("unknown").to_string();
|
||||
|
||||
let version_string: String = format!("Version {} built at {} revision {}", built_info::PKG_VERSION, built_info::BUILT_TIME_UTC, git_info);
|
||||
|
||||
msg.reply_ping(&ctx, &version_string).await.unwrap();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[command]
|
||||
async fn get_config(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult {
|
||||
let data = &ctx.data.read().await;
|
||||
let config = match data.get::<ManifoldConfig>() {
|
||||
Some(c) => c.lock().await,
|
||||
None => Err("Could not lock database!".to_string())?
|
||||
};
|
||||
|
||||
let key = match args.single::<String>() {
|
||||
Ok(k) => k,
|
||||
Err(e) => { msg.reply_ping(&ctx, format!("Failed to process your input: {:?}", e.to_string())).await?; Err("ArgParse")? }
|
||||
};
|
||||
|
||||
let value = match config.get_value(&key) {
|
||||
Ok(v) => v.clone(),
|
||||
Err(_) => "not found, sorry!".to_string()
|
||||
};
|
||||
|
||||
msg.reply_ping(&ctx, format!("Value for key {} was {}", &key, &value)).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[command]
|
||||
async fn set_config(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult {
|
||||
let mut data = ctx.data.write().await;
|
||||
let mut config = match data.get_mut::<ManifoldConfig>() {
|
||||
Some(c) => c.lock().await,
|
||||
None => Err("Could not lock database!".to_string())?
|
||||
};
|
||||
|
||||
let key = match args.single::<String>() {
|
||||
Ok(k) => k,
|
||||
Err(e) => { msg.reply_ping(&ctx, format!("Error parsing your message: {:?}", e)).await?; Err("ArgParse")? }
|
||||
};
|
||||
|
||||
let value = match args.single::<String>() {
|
||||
Ok(v) => v,
|
||||
Err(e) => { msg.reply_ping(&ctx, format!("Error parsing your message: {:?}", e)).await?; Err("ArgParse")? }
|
||||
};
|
||||
|
||||
match config.config.set(&key, value.clone()) {
|
||||
Ok(_) => msg.reply_ping(&ctx, format!("Value for key {} set to {}", &key, &value)).await?,
|
||||
Err(_) => msg.reply_ping(&ctx, format!("Error setting config value")).await?,
|
||||
};
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[command]
|
||||
async fn dump_config(ctx: &Context, msg: &Message, _args: Args) -> CommandResult {
|
||||
let data = &ctx.data.read().await;
|
||||
let config = match data.get::<ManifoldConfig>() {
|
||||
Some(c) => c.lock().await,
|
||||
None => Err("Could not lock database!".to_string())?
|
||||
};
|
||||
|
||||
msg.reply_ping(&ctx, format!("Config dump; {:?}", &config.config)).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
#[command]
|
||||
async fn get_environment(ctx: &Context, msg: &Message, _: Args) -> CommandResult {
|
||||
let data = ctx.data.read().await;
|
||||
let config = match data.get::<ManifoldConfig>() {
|
||||
Some(c) => c.lock().await,
|
||||
None => Err("Could not lock config!".to_string())?
|
||||
};
|
||||
|
||||
msg.reply_ping(&ctx, format!("Currently running under the {} environment", config.get_environment())).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
89
src/error.rs
89
src/error.rs
|
|
@ -1,89 +1,2 @@
|
|||
use std::error::Error;
|
||||
use std::fmt;
|
||||
use std::num::ParseIntError;
|
||||
use config::ConfigError;
|
||||
use serenity::prelude::SerenityError;
|
||||
use reqwest::Error as ReqwestError;
|
||||
|
||||
pub type ManifoldError = Box<dyn std::error::Error + Send + Sync>;
|
||||
pub type ManifoldResult<T> = Result<T, ManifoldError>;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct ManifoldError {
|
||||
pub details: String
|
||||
}
|
||||
|
||||
impl ManifoldError {
|
||||
pub fn new(msg: &str) -> Self {
|
||||
Self {
|
||||
details: msg.to_string()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for ManifoldError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "{}", self.details)
|
||||
}
|
||||
}
|
||||
|
||||
impl Error for ManifoldError {
|
||||
fn description(&self) -> &str {
|
||||
&self.details
|
||||
}
|
||||
}
|
||||
|
||||
impl From<()> for ManifoldError {
|
||||
fn from(_err: () ) -> Self {
|
||||
ManifoldError::new("Unspecified error")
|
||||
}
|
||||
}
|
||||
|
||||
impl From<r2d2::Error> for ManifoldError {
|
||||
fn from(err: r2d2::Error) -> Self {
|
||||
ManifoldError::new(&err.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<std::io::Error> for ManifoldError {
|
||||
fn from(err: std::io::Error) -> Self {
|
||||
ManifoldError::new(&err.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<serde_json::error::Error> for ManifoldError {
|
||||
fn from(err: serde_json::error::Error) -> Self {
|
||||
ManifoldError::new(&err.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<regex::Error> for ManifoldError {
|
||||
fn from(err: regex::Error) -> Self {
|
||||
ManifoldError::new(&err.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&str> for ManifoldError {
|
||||
fn from(err: &str) -> Self {
|
||||
ManifoldError::new(&err.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SerenityError> for ManifoldError {
|
||||
fn from(err: SerenityError) -> Self {
|
||||
ManifoldError::new(&err.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ReqwestError> for ManifoldError {
|
||||
fn from(err: ReqwestError) -> Self {
|
||||
ManifoldError::new(&err.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ConfigError> for ManifoldError {
|
||||
fn from(err: ConfigError) -> Self { ManifoldError::new(&err.to_string()) }
|
||||
}
|
||||
|
||||
impl From<ParseIntError> for ManifoldError {
|
||||
fn from(err: ParseIntError) -> Self { ManifoldError::new(&err.to_string()) }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,16 @@
|
|||
use std::sync::atomic::AtomicBool;
|
||||
|
||||
use serenity::async_trait;
|
||||
use serenity::model::{
|
||||
use poise::{Event, FrameworkContext};
|
||||
use poise::futures_util::future::ok;
|
||||
use poise::serenity_prelude::async_trait;
|
||||
use poise::serenity_prelude::model::{
|
||||
gateway::Ready,
|
||||
id::ChannelId,
|
||||
};
|
||||
use serenity::prelude::{Context, EventHandler};
|
||||
use crate::ManifoldConfig;
|
||||
use poise::serenity_prelude::{Context, EventHandler, Message};
|
||||
use crate::{ManifoldConfig, ManifoldContext, ManifoldData, ManifoldDataInner};
|
||||
use crate::error::{ManifoldError, ManifoldResult};
|
||||
use crate::models::user::UserInfo;
|
||||
|
||||
use crate::responses::Responses;
|
||||
|
||||
|
|
@ -21,26 +25,27 @@ impl Handler {
|
|||
}
|
||||
}
|
||||
|
||||
pub async fn standard_startup(ctx: &Context, data_about_bot: Ready) {
|
||||
let data = ctx.data.read().await;
|
||||
let config = match data.get::<ManifoldConfig>() {
|
||||
Some(c) => c.lock().await,
|
||||
None => return
|
||||
};
|
||||
pub async fn listen(ctx: &Context, framework_ctx: FrameworkContext<'_, ManifoldData, ManifoldError>, event: &Event<'_>) -> ManifoldResult<()> {
|
||||
match event {
|
||||
Event::Ready { data_about_bot} => Handler::standard_startup(&ctx, &framework_ctx, data_about_bot).await,
|
||||
Event::Message { new_message } => Handler::message(&ctx, &framework_ctx, &new_message).await,
|
||||
Event::MessageUpdate { old_if_available, new, event } => Handler::message_edited(&ctx, &framework_ctx, old_if_available, new).await,
|
||||
_ => Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
let responses = match data.get::<Responses>() {
|
||||
Some(r) => r.lock().await,
|
||||
None => return
|
||||
};
|
||||
pub async fn standard_startup(ctx: &Context, framework_ctx: &FrameworkContext<'_, ManifoldData, ManifoldError>, data_about_bot: &Ready) -> ManifoldResult<()> {
|
||||
let config = &framework_ctx.user_data().await.bot_config;
|
||||
let responses = &framework_ctx.user_data().await.responses;
|
||||
|
||||
let greeting = match responses.get_response(&"bot startup".to_string()) {
|
||||
Some(g) => g.replace("{NAME}", &*ctx.cache.current_user().name).replace("{COFFEE_TYPE}", "espresso").to_string(),
|
||||
Some(g) => g.to_owned(),
|
||||
None => "Manifold bot connected to discord and ready to begin broadcast operations.".to_string(),
|
||||
};
|
||||
|
||||
let bot_nickname = config.get_value(&"BotNickname".to_string()).unwrap_or("BrokenManifoldBot".to_string());
|
||||
let channel: ChannelId = config.get_channel(&"Log".to_string()).expect("Specified log channel invalid or unavailable");
|
||||
for guild in data_about_bot.guilds {
|
||||
for guild in &data_about_bot.guilds {
|
||||
match guild.id.edit_nickname(&ctx, Some(&*bot_nickname)).await {
|
||||
Ok(()) => (),
|
||||
Err(e) => {
|
||||
|
|
@ -50,12 +55,52 @@ impl Handler {
|
|||
}
|
||||
|
||||
channel.say(&ctx, greeting).await.expect("Couldn't message log channel!");
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl EventHandler for Handler {
|
||||
async fn ready(&self, ctx: Context, data_about_bot: Ready) {
|
||||
Handler::standard_startup(&ctx, data_about_bot).await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn message(_ctx: &Context, fctx: &FrameworkContext<'_, ManifoldData, ManifoldError>, msg: &Message) -> ManifoldResult<()> {
|
||||
let userinfo = &mut fctx.user_data().await.user_info.lock().await;
|
||||
|
||||
if let Some(u) = userinfo.get_mut(&msg.author.id.as_u64()) {
|
||||
u.last_seen = Some(chrono::Utc::now().timestamp());
|
||||
} else {
|
||||
let new_user = UserInfo {
|
||||
user_id: msg.author.id.as_u64().clone() as i64,
|
||||
username: msg.author.name.to_owned(),
|
||||
weather_location: None,
|
||||
weather_units: None,
|
||||
timezone: None,
|
||||
last_seen: Some(chrono::Utc::now().timestamp()),
|
||||
};
|
||||
|
||||
userinfo.insert(msg.author.id.as_u64().clone(), new_user);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn message_edited(ctx: &Context, fctx: &FrameworkContext<'_, ManifoldData, ManifoldError>, original: &Option<Message>, new_message: &Option<Message>) -> ManifoldResult<()> {
|
||||
let log_channel = fctx.user_data().await.bot_config.get_channel(&"Log".to_string()).unwrap();
|
||||
|
||||
if let Some(new) = new_message.as_ref() {
|
||||
log_channel.send_message(ctx, |f| {
|
||||
f
|
||||
.content("")
|
||||
.embed(|e| {
|
||||
e
|
||||
.title("Message updated")
|
||||
.author(|a| a.name(new.author.name.clone()))
|
||||
.timestamp(new.timestamp)
|
||||
.field("Original Content", match original.as_ref() {
|
||||
Some(m) => m.content.clone(),
|
||||
None => "Not available".to_string(),
|
||||
}, false)
|
||||
.field("New Content", new.content.clone(), false)
|
||||
})
|
||||
}).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
205
src/lib.rs
205
src/lib.rs
|
|
@ -1,37 +1,41 @@
|
|||
#[macro_use] extern crate log;
|
||||
#[macro_use] extern crate serde;
|
||||
|
||||
use std::collections::HashSet;
|
||||
use std::env;
|
||||
use std::ops::Deref;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use clap::ArgMatches;
|
||||
use diesel::r2d2::{ConnectionManager};
|
||||
use diesel::SqliteConnection;
|
||||
use serenity::prelude::*;
|
||||
use serenity::framework::standard::{*, macros::*};
|
||||
use serenity::http::Http;
|
||||
use serenity::model::prelude::{GuildId, Message, UserId};
|
||||
use crate::config::ManifoldConfig;
|
||||
|
||||
use crate::error::ManifoldResult;
|
||||
use clap::ArgMatches;
|
||||
use diesel::r2d2::ConnectionManager;
|
||||
use diesel::sqlite::Sqlite;
|
||||
use diesel::SqliteConnection;
|
||||
use diesel_migrations::{embed_migrations, EmbeddedMigrations, MigrationHarness};
|
||||
use poise::framework::FrameworkBuilder;
|
||||
use poise::serenity_prelude::*;
|
||||
|
||||
use crate::config::ManifoldConfig;
|
||||
use crate::error::{ManifoldError, ManifoldResult};
|
||||
use crate::events::Handler;
|
||||
use crate::models::user::{ManifoldUserInfo, UserInfo};
|
||||
use crate::responses::Responses;
|
||||
|
||||
pub mod config;
|
||||
pub mod error;
|
||||
pub mod responses;
|
||||
pub mod events;
|
||||
pub mod core_commands;
|
||||
pub mod responses;
|
||||
pub mod commands;
|
||||
pub mod models;
|
||||
pub mod schema;
|
||||
|
||||
pub const MIGRATIONS: EmbeddedMigrations = embed_migrations!("migrations");
|
||||
|
||||
// Retrieve build info from output file
|
||||
pub mod built_info {
|
||||
include!(concat!(env!("OUT_DIR"), "/built.rs"));
|
||||
}
|
||||
|
||||
use crate::core_commands::*;
|
||||
|
||||
pub type ManifoldDatabasePool = diesel::r2d2::Pool<ConnectionManager<SqliteConnection>>;
|
||||
pub type ManifoldDatabasePool = r2d2::Pool<ConnectionManager<SqliteConnection>>;
|
||||
|
||||
pub struct Db {
|
||||
pub pool: ManifoldDatabasePool,
|
||||
|
|
@ -45,29 +49,33 @@ impl Deref for Db {
|
|||
|
||||
pub struct ManifoldDatabase;
|
||||
|
||||
impl TypeMapKey for ManifoldDatabase {
|
||||
type Value = Arc<Mutex<Db>>;
|
||||
pub struct ManifoldData(pub Arc<ManifoldDataInner>);
|
||||
|
||||
impl Deref for ManifoldData {
|
||||
type Target = ManifoldDataInner;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl TypeMapKey for Responses {
|
||||
type Value = Arc<Mutex<Responses>>;
|
||||
pub struct ManifoldDataInner {
|
||||
bot_config: ManifoldConfig,
|
||||
database: Db,
|
||||
responses: Responses,
|
||||
user_info: Mutex<ManifoldUserInfo>,
|
||||
}
|
||||
|
||||
impl TypeMapKey for ManifoldConfig {
|
||||
type Value = Arc<Mutex<ManifoldConfig>>;
|
||||
}
|
||||
pub type ManifoldContext<'a> = poise::Context<'a, ManifoldData, ManifoldError>;
|
||||
pub type ManifoldCommand = poise::Command<ManifoldData, ManifoldError>;
|
||||
|
||||
#[group]
|
||||
#[commands(ping, set_config, get_config, dump_config, version, set_activity, get_environment)]
|
||||
struct Core;
|
||||
|
||||
pub async fn prepare_client<T: 'static + EventHandler>(arguments: ArgMatches, mut framework: StandardFramework, event_handler: T, intents: GatewayIntents) -> ManifoldResult<Client> {
|
||||
let bot_environment = arguments.value_of("environment").unwrap_or("Production").to_string();
|
||||
let config_file = format!("config/{}", arguments.value_of("config-file").unwrap_or("manifold.yaml"));
|
||||
pub async fn prepare_client(arguments: ArgMatches, intents: GatewayIntents, injected_commands: Vec<ManifoldCommand>) -> ManifoldResult<FrameworkBuilder<ManifoldData, ManifoldError>> {
|
||||
let bot_environment = arguments.get_one("environment").unwrap();
|
||||
let config_file = format!("config/{}", arguments.get_one::<String>("config-file").unwrap());
|
||||
|
||||
info!("Reading configuration...");
|
||||
debug!("Configuration file path: {}", &config_file);
|
||||
let config = ManifoldConfig::load_config(&config_file, &bot_environment).expect(&*format!("Could not read configuration file {}", &config_file));
|
||||
let config = ManifoldConfig::load_config(&config_file, bot_environment).expect(&*format!("Could not read configuration file {}", &config_file));
|
||||
|
||||
let prefix = config.get_value(&"BotPrefix".to_string()).expect("Could not read bot_prefix from config.");
|
||||
|
||||
|
|
@ -80,110 +88,51 @@ pub async fn prepare_client<T: 'static + EventHandler>(arguments: ArgMatches, mu
|
|||
let mut responses = Responses::new();
|
||||
responses.reload(&PathBuf::from(config.get_value(&"ResponsesFilePath".to_string()).unwrap_or("txt/responses.txt".to_string()))).expect("Could not load responses file!");
|
||||
|
||||
|
||||
let token = env::var("DISCORD_TOKEN").expect(
|
||||
"Could not find an environment variable called DISCORD_TOKEN",
|
||||
);
|
||||
|
||||
let application_id: u64 = env::var("APPLICATION_ID").expect(
|
||||
"Could not find an application ID in the APPLICATION_ID environment variable, please provide one",
|
||||
).parse().expect("That wasn't a number");
|
||||
|
||||
let http = Http::new(&token);
|
||||
|
||||
let owners = match http.get_current_application_info().await {
|
||||
Ok(info) => {
|
||||
let mut owners = HashSet::new();
|
||||
owners.insert(info.owner.id);
|
||||
|
||||
owners
|
||||
}
|
||||
Err(why) => panic!("Could not get HTTP application information - exiting: {:?}", why),
|
||||
};
|
||||
|
||||
framework = framework.configure(|c| c
|
||||
.with_whitespace(true)
|
||||
.prefix(&prefix)
|
||||
.owners(owners)
|
||||
.case_insensitivity(true)
|
||||
).before(before)
|
||||
.help(&MANIFOLD_HELP);
|
||||
framework.group_add(&CORE_GROUP);
|
||||
|
||||
let client = Client::builder(&token, intents)
|
||||
.event_handler(event_handler)
|
||||
.application_id(application_id)
|
||||
.framework(framework)
|
||||
.await
|
||||
.expect("Error creating client!");
|
||||
{
|
||||
let mut data = client.data.write().await;
|
||||
let framework = poise::Framework::builder()
|
||||
.options(poise::FrameworkOptions {
|
||||
event_handler: |ctx, e, fctx, _| Box::pin(async move {
|
||||
Handler::listen(ctx, fctx, e).await
|
||||
}),
|
||||
pre_command: |ctx: ManifoldContext<'_>| Box::pin(async move {
|
||||
info!("Received command {} from {}", ctx.command().name, ctx.author().name);
|
||||
let config = &ctx.data().bot_config;
|
||||
let log_channel = config.get_channel(&"Log".to_string()).unwrap();
|
||||
let _ = log_channel.say(ctx, format!("Received command {} from {}", ctx.command().name, ctx.author().name)).await;
|
||||
}),
|
||||
commands: commands::collect_commands(injected_commands),
|
||||
prefix_options: poise::PrefixFrameworkOptions {
|
||||
prefix: Some(prefix),
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
})
|
||||
.token(token)
|
||||
.intents(intents)
|
||||
.setup(|ctx, _ready, framework| {
|
||||
Box::pin(async move {
|
||||
poise::builtins::register_globally(ctx, &framework.options().commands).await?;
|
||||
ctx.set_activity(Activity::watching("you")).await;
|
||||
let db = Db { pool };
|
||||
apply_migrations(&mut db.get()?);
|
||||
let user_info = UserInfo::load(&db).expect("Could not load user info, rejecting");
|
||||
Ok(ManifoldData(Arc::new(ManifoldDataInner {
|
||||
bot_config: config,
|
||||
database: db,
|
||||
responses,
|
||||
user_info: Mutex::new(user_info),
|
||||
})))
|
||||
})
|
||||
});
|
||||
|
||||
data.insert::<ManifoldConfig>(Arc::new(Mutex::new(config)));
|
||||
data.insert::<ManifoldDatabase>(Arc::new(Mutex::new(db)));
|
||||
data.insert::<Responses>(Arc::new(Mutex::new(responses)));
|
||||
}
|
||||
|
||||
Ok(client)
|
||||
Ok(framework)
|
||||
}
|
||||
|
||||
#[help]
|
||||
async fn manifold_help(
|
||||
ctx: &Context,
|
||||
msg: &Message,
|
||||
args: Args,
|
||||
help_options: &'static HelpOptions,
|
||||
groups: &[&'static CommandGroup],
|
||||
owners: HashSet<UserId>
|
||||
) -> CommandResult {
|
||||
let _ = help_commands::with_embeds(ctx, msg, args, help_options, groups, owners).await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[check]
|
||||
#[name = "ModOrHigher"]
|
||||
async fn mod_or_higher_check(ctx: &Context, msg: &Message, _: &mut Args, _: &CommandOptions) -> Result<(), Reason> {
|
||||
let data = ctx.data.read().await;
|
||||
let config = match data.get::<ManifoldConfig>() {
|
||||
Some(c) => c.lock().await,
|
||||
None => return Err(Reason::Log("Couldn't lock config".to_string()))
|
||||
};
|
||||
|
||||
let role_guild_config = match config.get_value(&"RoleGuild".to_string()) {
|
||||
Ok(rgc) => rgc,
|
||||
Err(_) => {
|
||||
error!("Guild not configured");
|
||||
return Err(Reason::Unknown);
|
||||
}
|
||||
};
|
||||
|
||||
let role_guild_number = match role_guild_config.parse::<u64>() {
|
||||
Ok(rgn) => rgn,
|
||||
Err(e) => {
|
||||
error!("Parse GuildId fail");
|
||||
return Err(Reason::Log(e.to_string()));
|
||||
}
|
||||
};
|
||||
|
||||
let guild_id = GuildId(role_guild_number);
|
||||
let administrator_role = match config.get_role(&"Admin".to_string()) {
|
||||
Ok(r) => r,
|
||||
Err(e) => {
|
||||
error!("Get admin role from config failed: {:?}", e);
|
||||
return Err(Reason::Log(e.to_string()));
|
||||
}
|
||||
};
|
||||
|
||||
if !msg.author.has_role(&ctx, guild_id, administrator_role).await.unwrap_or(false) {
|
||||
return Err(Reason::User("User not administrative".to_string()));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[hook]
|
||||
async fn before(_: &Context, msg: &Message, command_name: &str) -> bool {
|
||||
info!("Received command '{}' from user '{}'", command_name, msg.author.name);
|
||||
true
|
||||
fn apply_migrations(conn: &mut impl MigrationHarness<Sqlite>) {
|
||||
|
||||
conn.run_pending_migrations(MIGRATIONS)
|
||||
.expect("An error occurred applying migrations.");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,2 @@
|
|||
pub mod user;
|
||||
pub mod weather;
|
||||
|
|
@ -0,0 +1,62 @@
|
|||
use diesel::prelude::*;
|
||||
use std::collections::HashMap;
|
||||
use diesel::{insert_into, update};
|
||||
|
||||
use crate::schema::*;
|
||||
use crate::schema::userinfo::dsl as userinfo_dsl;
|
||||
use crate::Db;
|
||||
use crate::error::{ManifoldError, ManifoldResult};
|
||||
|
||||
#[derive(Identifiable, Insertable, AsChangeset, Debug, Queryable, Serialize, Clone)]
|
||||
#[diesel(primary_key(user_id))]
|
||||
#[diesel(table_name = userinfo)]
|
||||
pub struct UserInfo {
|
||||
pub(crate) user_id: i64,
|
||||
pub username: String,
|
||||
pub weather_location: Option<String>,
|
||||
pub weather_units: Option<String>,
|
||||
pub timezone: Option<String>,
|
||||
pub last_seen: Option<i64>,
|
||||
}
|
||||
|
||||
pub type ManifoldUserInfo = HashMap<u64, UserInfo>;
|
||||
|
||||
impl UserInfo {
|
||||
pub fn load(conn: &Db) -> ManifoldResult<ManifoldUserInfo> {
|
||||
let data = userinfo_dsl::userinfo
|
||||
.get_results::<Self>(&mut conn.get()?);
|
||||
|
||||
debug!("{:?}", data);
|
||||
|
||||
let mut loaded_userinfo = ManifoldUserInfo::new();
|
||||
|
||||
match data {
|
||||
Ok(d) => {
|
||||
for user in d {
|
||||
loaded_userinfo.insert(user.user_id as u64, user.clone());
|
||||
};
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Couldn't load user data! {:?}", e);
|
||||
Err("Couldn't load user data")?
|
||||
}
|
||||
};
|
||||
|
||||
Ok(loaded_userinfo)
|
||||
}
|
||||
|
||||
pub fn insert(&self, conn: &Db) -> ManifoldResult<usize> {
|
||||
insert_into(userinfo_dsl::userinfo)
|
||||
.values(self)
|
||||
.execute(&mut conn.get()?)
|
||||
.map_err(|e| ManifoldError::from(e))
|
||||
}
|
||||
|
||||
pub fn save(&self, conn: &Db) -> ManifoldResult<usize> {
|
||||
update(userinfo_dsl::userinfo)
|
||||
.filter(userinfo::user_id.eq(self.user_id))
|
||||
.set(self)
|
||||
.execute(&mut conn.get()?)
|
||||
.map_err(|e| ManifoldError::from(e))
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,186 @@
|
|||
use crate::error::ManifoldResult;
|
||||
use reqwest::Client;
|
||||
use reqwest::header::CONTENT_TYPE;
|
||||
|
||||
pub struct Weather {
|
||||
base_url: String,
|
||||
api_key: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct CurrentConditionInformation {
|
||||
pub text: String,
|
||||
pub icon: String,
|
||||
pub code: i64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct CurrentWeatherInformation {
|
||||
pub last_updated: String,
|
||||
pub last_updated_epoch: i64,
|
||||
pub temp_c: f64,
|
||||
pub temp_f: f64,
|
||||
pub feelslike_c: f64,
|
||||
pub feelslike_f: f64,
|
||||
pub condition: CurrentConditionInformation,
|
||||
pub wind_mph: f64,
|
||||
pub wind_kph: f64,
|
||||
pub wind_degree: i64,
|
||||
pub wind_dir: String,
|
||||
pub pressure_mb: f64,
|
||||
pub pressure_in: f64,
|
||||
pub precip_mm: f64,
|
||||
pub precip_in: f64,
|
||||
pub humidity: i64,
|
||||
pub cloud: i64,
|
||||
pub is_day: i64,
|
||||
pub uv: f64,
|
||||
pub gust_mph: f64,
|
||||
pub gust_kph: f64,
|
||||
pub dewpoint_c: Option<f64>,
|
||||
pub dewpoint_f: Option<f64>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct ForecastHour {
|
||||
pub time_epoch: i64,
|
||||
pub time: String,
|
||||
pub temp_c: f64,
|
||||
pub temp_f: f64,
|
||||
pub condition: CurrentConditionInformation,
|
||||
pub wind_mph: f64,
|
||||
pub wind_kph: f64,
|
||||
pub wind_degree: i64,
|
||||
pub wind_dir: String,
|
||||
pub pressure_mb: f64,
|
||||
pub pressure_in: f64,
|
||||
pub precip_mm: f64,
|
||||
pub precip_in: f64,
|
||||
pub humidity: i64,
|
||||
pub cloud: i64,
|
||||
pub feelslike_c: f64,
|
||||
pub feelslike_f: f64,
|
||||
pub windchill_c: f64,
|
||||
pub windchill_f: f64,
|
||||
pub heatindex_c: f64,
|
||||
pub heatindex_f: f64,
|
||||
pub dewpoint_c: f64,
|
||||
pub dewpoint_f: f64,
|
||||
pub will_it_rain: i64,
|
||||
pub will_it_snow: i64,
|
||||
pub is_day: i64,
|
||||
pub vis_km: f64,
|
||||
pub vis_miles: f64,
|
||||
pub chance_of_rain: i64,
|
||||
pub chance_of_snow: i64,
|
||||
pub gust_mph: f64,
|
||||
pub gust_kph: f64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct DayWeather {
|
||||
pub maxtemp_c: f64,
|
||||
pub maxtemp_f: f64,
|
||||
pub mintemp_c: f64,
|
||||
pub mintemp_f: f64,
|
||||
pub avgtemp_c: f64,
|
||||
pub avgtemp_f: f64,
|
||||
pub maxwind_mph: f64,
|
||||
pub maxwind_kph: f64,
|
||||
pub totalprecip_mm: f64,
|
||||
pub totalprecip_in: f64,
|
||||
pub avgvis_km: f64,
|
||||
pub avgvis_miles: f64,
|
||||
pub avghumidity: f64,
|
||||
pub condition: CurrentConditionInformation,
|
||||
pub uv: f64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct DayAstro {
|
||||
pub sunrise: String,
|
||||
pub sunset: String,
|
||||
pub moonrise: String,
|
||||
pub moonset: String,
|
||||
pub moon_phase: String,
|
||||
pub moon_illumination: String
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct ForecastDayWrapper {
|
||||
pub forecastday: Vec<ForecastDay>
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct ForecastDay {
|
||||
pub date: String,
|
||||
pub date_epoch: i64,
|
||||
pub day: DayWeather,
|
||||
pub astro: DayAstro,
|
||||
pub hour: Vec<ForecastHour>
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct LocationInformation {
|
||||
pub name: String,
|
||||
pub region: String,
|
||||
pub country: String,
|
||||
pub lat: f64,
|
||||
pub lon: f64,
|
||||
pub tz_id: String,
|
||||
pub localtime_epoch: i64,
|
||||
pub localtime: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct WeatherRequestResponse {
|
||||
pub location: LocationInformation,
|
||||
pub current: CurrentWeatherInformation,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct WeatherForecastRequestResponse {
|
||||
pub location: LocationInformation,
|
||||
pub current: CurrentWeatherInformation,
|
||||
pub forecast: ForecastDayWrapper,
|
||||
}
|
||||
|
||||
impl Weather {
|
||||
pub fn new(api_key:String, base_url: String) -> Self {
|
||||
Weather {
|
||||
base_url,
|
||||
api_key,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_weather_forecast(&self, target: String, days: Option<i32>) -> ManifoldResult<WeatherForecastRequestResponse> {
|
||||
let target_url: String;
|
||||
|
||||
if let Some(d) = days {
|
||||
target_url = format!("{}/forecast.json?key={}&q={}&days={}", self.base_url, self.api_key, &target, d);
|
||||
} else {
|
||||
target_url = format!("{}/forecast.json?key={}&q={}&days=3", self.base_url, self.api_key, &target);
|
||||
}
|
||||
|
||||
let client = Client::new();
|
||||
let result = client.get(target_url)
|
||||
.header(CONTENT_TYPE, "application/json")
|
||||
.send()
|
||||
.await?
|
||||
.text()
|
||||
.await?;
|
||||
|
||||
debug!("{:?}", result);
|
||||
|
||||
let mut decoded_result: WeatherForecastRequestResponse = serde_json::from_str(&*result)?;
|
||||
|
||||
let temp = decoded_result.current.temp_c; let humid = decoded_result.current.humidity as f64;
|
||||
decoded_result.current.dewpoint_c = Some(&temp - (14.55 + 0.114 * &temp) * (1.0 - (0.01 * &humid)) - num::pow((2.5 + 0.007 * &temp) * (1.0 - (0.01 * &humid)),3) - (15.9 + 0.117 * &temp) * num::pow(1.0 - (0.01 * &humid), 14));
|
||||
decoded_result.current.dewpoint_f = Some((decoded_result.current.dewpoint_c.unwrap() * 1.8) + 32.0);
|
||||
|
||||
|
||||
debug!("{:?}", decoded_result);
|
||||
|
||||
Ok(decoded_result)
|
||||
}
|
||||
}
|
||||
|
|
@ -8,6 +8,7 @@ use rand::seq::SliceRandom;
|
|||
use crate::error::ManifoldResult;
|
||||
use std::io::{BufReader, BufRead};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Responses {
|
||||
file_content: HashMap<String, Vec<String>>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,12 @@
|
|||
// @generated automatically by Diesel CLI.
|
||||
|
||||
diesel::table! {
|
||||
userinfo (user_id) {
|
||||
user_id -> BigInt,
|
||||
username -> Text,
|
||||
weather_location -> Nullable<Text>,
|
||||
weather_units -> Nullable<Text>,
|
||||
timezone -> Nullable<Text>,
|
||||
last_seen -> Nullable<BigInt>,
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue