Move over to phorge, take this with me

This commit is contained in:
Xyon 2023-08-23 18:37:42 +01:00
parent cc4ea42992
commit c255e5b2f3
Signed by: xyon
GPG Key ID: DD18155D6B18078D
18 changed files with 726 additions and 370 deletions

1
.gitignore vendored
View File

@ -1,3 +1,4 @@
/target /target
.idea/ .idea/
Cargo.lock Cargo.lock
*.db

View File

@ -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"] }

9
diesel.toml Normal file
View File

@ -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"

View File

@ -0,0 +1 @@
DROP TABLE "userinfo";

View File

@ -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
);

98
src/commands/core.rs Normal file
View File

@ -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()]
}

11
src/commands/mod.rs Normal file
View File

@ -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()
}

174
src/commands/weather.rs Normal file
View File

@ -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()]
}

View File

@ -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,

View File

@ -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(())
}

View File

@ -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()) }
}

View File

@ -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(())
} }
} }

View File

@ -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
let http = Http::new(&token); }),
pre_command: |ctx: ManifoldContext<'_>| Box::pin(async move {
let owners = match http.get_current_application_info().await { info!("Received command {} from {}", ctx.command().name, ctx.author().name);
Ok(info) => { let config = &ctx.data().bot_config;
let mut owners = HashSet::new(); let log_channel = config.get_channel(&"Log".to_string()).unwrap();
owners.insert(info.owner.id); let _ = log_channel.say(ctx, format!("Received command {} from {}", ctx.command().name, ctx.author().name)).await;
}),
owners commands: commands::collect_commands(injected_commands),
} prefix_options: poise::PrefixFrameworkOptions {
Err(why) => panic!("Could not get HTTP application information - exiting: {:?}", why), prefix: Some(prefix),
}; ..Default::default()
},
framework = framework.configure(|c| c ..Default::default()
.with_whitespace(true) })
.prefix(&prefix) .token(token)
.owners(owners) .intents(intents)
.case_insensitivity(true) .setup(|ctx, _ready, framework| {
).before(before) Box::pin(async move {
.help(&MANIFOLD_HELP); poise::builtins::register_globally(ctx, &framework.options().commands).await?;
framework.group_add(&CORE_GROUP); ctx.set_activity(Activity::watching("you")).await;
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 }; 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))); Ok(framework)
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
} }

2
src/models/mod.rs Normal file
View File

@ -0,0 +1,2 @@
pub mod user;
pub mod weather;

62
src/models/user.rs Normal file
View File

@ -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))
}
}

186
src/models/weather.rs Normal file
View File

@ -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)
}
}

View File

@ -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>>
} }

12
src/schema.rs Normal file
View File

@ -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>,
}
}