Move over to phorge, take this with me
This commit is contained in:
parent
cc4ea42992
commit
c255e5b2f3
|
|
@ -1,3 +1,4 @@
|
||||||
/target
|
/target
|
||||||
.idea/
|
.idea/
|
||||||
Cargo.lock
|
Cargo.lock
|
||||||
|
*.db
|
||||||
|
|
|
||||||
21
Cargo.toml
21
Cargo.toml
|
|
@ -1,27 +1,30 @@
|
||||||
[package]
|
[package]
|
||||||
name = "manifold"
|
name = "manifold"
|
||||||
version = "0.1.0"
|
version = "1.0.0"
|
||||||
authors = ["Lucy Bladen <admin@jbladen.uk>"]
|
authors = ["Lucy Bladen <admin@lbladen.uk>"]
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
built = { version = "0.5.1", features = ["git2", "chrono"] }
|
built = { version = "0.6.0", features = ["git2", "chrono"] }
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
built = { version = "0.5.1", features = ["git2", "chrono"] }
|
built = { version = "0.6.0", features = ["git2", "chrono"] }
|
||||||
clap = "3.2.10"
|
chrono = "0.4.26"
|
||||||
|
clap = "4.3.4"
|
||||||
config = { version = "0.13.1", features = [ "yaml" ] }
|
config = { version = "0.13.1", features = [ "yaml" ] }
|
||||||
diesel = { version = "1.4.8", features = ["sqlite", "r2d2", "chrono"] }
|
d20 = "0.1.0"
|
||||||
diesel_migrations = "1.4.0"
|
diesel = { version = "2.1.0", features = ["sqlite", "r2d2", "chrono"] }
|
||||||
env_logger = "0.9.0"
|
diesel_migrations = "2.1.0"
|
||||||
|
env_logger = "0.10.0"
|
||||||
log = "0.4.14"
|
log = "0.4.14"
|
||||||
|
num = "0.4.1"
|
||||||
|
poise = "0.5.5"
|
||||||
r2d2 = "0.8.9"
|
r2d2 = "0.8.9"
|
||||||
rand = "0.8.5"
|
rand = "0.8.5"
|
||||||
regex = "1.5.4"
|
regex = "1.5.4"
|
||||||
reqwest = "0.11.9"
|
reqwest = "0.11.9"
|
||||||
serde = "1.0.136"
|
serde = "1.0.136"
|
||||||
serde_json = "1.0.79"
|
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"] }
|
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 std::path::PathBuf;
|
||||||
use config::Config;
|
use config::Config;
|
||||||
use serenity::model::prelude::{ChannelId, MessageId, RoleId};
|
use poise::serenity_prelude::model::prelude::{ChannelId, MessageId, RoleId};
|
||||||
use crate::ManifoldResult;
|
use crate::ManifoldResult;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
pub struct ManifoldConfig {
|
pub struct ManifoldConfig {
|
||||||
pub environment: String,
|
pub environment: String,
|
||||||
pub path: PathBuf,
|
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;
|
pub type ManifoldError = Box<dyn std::error::Error + Send + Sync>;
|
||||||
use std::fmt;
|
|
||||||
use std::num::ParseIntError;
|
|
||||||
use config::ConfigError;
|
|
||||||
use serenity::prelude::SerenityError;
|
|
||||||
use reqwest::Error as ReqwestError;
|
|
||||||
|
|
||||||
pub type ManifoldResult<T> = Result<T, ManifoldError>;
|
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 std::sync::atomic::AtomicBool;
|
||||||
|
|
||||||
use serenity::async_trait;
|
use poise::{Event, FrameworkContext};
|
||||||
use serenity::model::{
|
use poise::futures_util::future::ok;
|
||||||
|
use poise::serenity_prelude::async_trait;
|
||||||
|
use poise::serenity_prelude::model::{
|
||||||
gateway::Ready,
|
gateway::Ready,
|
||||||
id::ChannelId,
|
id::ChannelId,
|
||||||
};
|
};
|
||||||
use serenity::prelude::{Context, EventHandler};
|
use poise::serenity_prelude::{Context, EventHandler, Message};
|
||||||
use crate::ManifoldConfig;
|
use crate::{ManifoldConfig, ManifoldContext, ManifoldData, ManifoldDataInner};
|
||||||
|
use crate::error::{ManifoldError, ManifoldResult};
|
||||||
|
use crate::models::user::UserInfo;
|
||||||
|
|
||||||
use crate::responses::Responses;
|
use crate::responses::Responses;
|
||||||
|
|
||||||
|
|
@ -21,26 +25,27 @@ impl Handler {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn standard_startup(ctx: &Context, data_about_bot: Ready) {
|
pub async fn listen(ctx: &Context, framework_ctx: FrameworkContext<'_, ManifoldData, ManifoldError>, event: &Event<'_>) -> ManifoldResult<()> {
|
||||||
let data = ctx.data.read().await;
|
match event {
|
||||||
let config = match data.get::<ManifoldConfig>() {
|
Event::Ready { data_about_bot} => Handler::standard_startup(&ctx, &framework_ctx, data_about_bot).await,
|
||||||
Some(c) => c.lock().await,
|
Event::Message { new_message } => Handler::message(&ctx, &framework_ctx, &new_message).await,
|
||||||
None => return
|
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>() {
|
pub async fn standard_startup(ctx: &Context, framework_ctx: &FrameworkContext<'_, ManifoldData, ManifoldError>, data_about_bot: &Ready) -> ManifoldResult<()> {
|
||||||
Some(r) => r.lock().await,
|
let config = &framework_ctx.user_data().await.bot_config;
|
||||||
None => return
|
let responses = &framework_ctx.user_data().await.responses;
|
||||||
};
|
|
||||||
|
|
||||||
let greeting = match responses.get_response(&"bot startup".to_string()) {
|
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(),
|
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 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");
|
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 {
|
match guild.id.edit_nickname(&ctx, Some(&*bot_nickname)).await {
|
||||||
Ok(()) => (),
|
Ok(()) => (),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
|
|
@ -50,12 +55,52 @@ impl Handler {
|
||||||
}
|
}
|
||||||
|
|
||||||
channel.say(&ctx, greeting).await.expect("Couldn't message log channel!");
|
channel.say(&ctx, greeting).await.expect("Couldn't message log channel!");
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait]
|
Ok(())
|
||||||
impl EventHandler for Handler {
|
}
|
||||||
async fn ready(&self, ctx: Context, data_about_bot: Ready) {
|
|
||||||
Handler::standard_startup(&ctx, data_about_bot).await;
|
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(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
207
src/lib.rs
207
src/lib.rs
|
|
@ -1,37 +1,41 @@
|
||||||
#[macro_use] extern crate log;
|
#[macro_use] extern crate log;
|
||||||
#[macro_use] extern crate serde;
|
#[macro_use] extern crate serde;
|
||||||
|
|
||||||
use std::collections::HashSet;
|
|
||||||
use std::env;
|
use std::env;
|
||||||
use std::ops::Deref;
|
use std::ops::Deref;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::sync::Arc;
|
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;
|
use crate::responses::Responses;
|
||||||
|
|
||||||
pub mod config;
|
pub mod config;
|
||||||
pub mod error;
|
pub mod error;
|
||||||
pub mod responses;
|
|
||||||
pub mod events;
|
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
|
// Retrieve build info from output file
|
||||||
pub mod built_info {
|
pub mod built_info {
|
||||||
include!(concat!(env!("OUT_DIR"), "/built.rs"));
|
include!(concat!(env!("OUT_DIR"), "/built.rs"));
|
||||||
}
|
}
|
||||||
|
|
||||||
use crate::core_commands::*;
|
pub type ManifoldDatabasePool = r2d2::Pool<ConnectionManager<SqliteConnection>>;
|
||||||
|
|
||||||
pub type ManifoldDatabasePool = diesel::r2d2::Pool<ConnectionManager<SqliteConnection>>;
|
|
||||||
|
|
||||||
pub struct Db {
|
pub struct Db {
|
||||||
pub pool: ManifoldDatabasePool,
|
pub pool: ManifoldDatabasePool,
|
||||||
|
|
@ -45,29 +49,33 @@ impl Deref for Db {
|
||||||
|
|
||||||
pub struct ManifoldDatabase;
|
pub struct ManifoldDatabase;
|
||||||
|
|
||||||
impl TypeMapKey for ManifoldDatabase {
|
pub struct ManifoldData(pub Arc<ManifoldDataInner>);
|
||||||
type Value = Arc<Mutex<Db>>;
|
|
||||||
|
impl Deref for ManifoldData {
|
||||||
|
type Target = ManifoldDataInner;
|
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TypeMapKey for Responses {
|
pub struct ManifoldDataInner {
|
||||||
type Value = Arc<Mutex<Responses>>;
|
bot_config: ManifoldConfig,
|
||||||
|
database: Db,
|
||||||
|
responses: Responses,
|
||||||
|
user_info: Mutex<ManifoldUserInfo>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TypeMapKey for ManifoldConfig {
|
pub type ManifoldContext<'a> = poise::Context<'a, ManifoldData, ManifoldError>;
|
||||||
type Value = Arc<Mutex<ManifoldConfig>>;
|
pub type ManifoldCommand = poise::Command<ManifoldData, ManifoldError>;
|
||||||
}
|
|
||||||
|
|
||||||
#[group]
|
pub async fn prepare_client(arguments: ArgMatches, intents: GatewayIntents, injected_commands: Vec<ManifoldCommand>) -> ManifoldResult<FrameworkBuilder<ManifoldData, ManifoldError>> {
|
||||||
#[commands(ping, set_config, get_config, dump_config, version, set_activity, get_environment)]
|
let bot_environment = arguments.get_one("environment").unwrap();
|
||||||
struct Core;
|
let config_file = format!("config/{}", arguments.get_one::<String>("config-file").unwrap());
|
||||||
|
|
||||||
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"));
|
|
||||||
|
|
||||||
info!("Reading configuration...");
|
info!("Reading configuration...");
|
||||||
debug!("Configuration file path: {}", &config_file);
|
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.");
|
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();
|
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!");
|
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(
|
let token = env::var("DISCORD_TOKEN").expect(
|
||||||
"Could not find an environment variable called DISCORD_TOKEN",
|
"Could not find an environment variable called DISCORD_TOKEN",
|
||||||
);
|
);
|
||||||
|
|
||||||
let application_id: u64 = env::var("APPLICATION_ID").expect(
|
let framework = poise::Framework::builder()
|
||||||
"Could not find an application ID in the APPLICATION_ID environment variable, please provide one",
|
.options(poise::FrameworkOptions {
|
||||||
).parse().expect("That wasn't a number");
|
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),
|
||||||
|
})))
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
let http = Http::new(&token);
|
Ok(framework)
|
||||||
|
|
||||||
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 db = Db { pool };
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[help]
|
fn apply_migrations(conn: &mut impl MigrationHarness<Sqlite>) {
|
||||||
async fn manifold_help(
|
|
||||||
ctx: &Context,
|
conn.run_pending_migrations(MIGRATIONS)
|
||||||
msg: &Message,
|
.expect("An error occurred applying migrations.");
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 crate::error::ManifoldResult;
|
||||||
use std::io::{BufReader, BufRead};
|
use std::io::{BufReader, BufRead};
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
pub struct Responses {
|
pub struct Responses {
|
||||||
file_content: HashMap<String, Vec<String>>
|
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