Merge pull request #10 from Trivernis/develop

Develop
pull/16/head v0.5.0
Trivernis 3 years ago committed by GitHub
commit ad69f78bd6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

6
Cargo.lock generated

@ -180,7 +180,7 @@ dependencies = [
[[package]]
name = "bot-coreutils"
version = "0.1.0"
version = "0.1.1"
dependencies = [
"log 0.4.14",
"mime_guess",
@ -191,7 +191,7 @@ dependencies = [
[[package]]
name = "bot-database"
version = "0.1.0"
version = "0.3.0"
dependencies = [
"chrono",
"diesel",
@ -2282,7 +2282,7 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c"
[[package]]
name = "tobi-rs"
version = "0.4.0"
version = "0.5.0"
dependencies = [
"aspotify",
"bot-coreutils",

@ -1,6 +1,6 @@
[package]
name = "tobi-rs"
version = "0.4.0"
version = "0.5.0"
authors = ["trivernis <trivernis@protonmail.com>"]
edition = "2018"

@ -8,7 +8,7 @@ checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a"
[[package]]
name = "bot-coreutils"
version = "0.1.0"
version = "0.1.1"
dependencies = [
"log",
"mime_guess",

@ -1,6 +1,6 @@
[package]
name = "bot-coreutils"
version = "0.1.0"
version = "0.1.1"
authors = ["trivernis <trivernis@protonmail.com>"]
edition = "2018"

@ -0,0 +1,9 @@
use rand::seq::SliceRandom;
/// Chooses a random value from the given iterator
/// panics when the iterator is empty
pub fn choose_unchecked<'a, I: SliceRandom<Item = &'a T>, T>(i: I) -> &'a T {
let mut rng = rand::thread_rng();
i.choose(&mut rng).unwrap()
}

@ -35,3 +35,10 @@ fn it_checks_for_video() {
assert!(!is_video("https://domain.com/file.pdf"));
assert!(!is_video("not an url"));
}
#[test]
fn it_checks_if_its_valid() {
assert!(is_valid("https://domain.com"));
assert!(!is_valid("domain.com"));
assert!(is_valid("https://url.com/sub/sub/sub.txt"))
}

@ -69,3 +69,8 @@ pub fn is_video(url_str: &str) -> bool {
false
}
}
/// Returns if the given url is valid
pub fn is_valid(url_str: &str) -> bool {
Url::parse(url_str).is_ok()
}

@ -1,6 +1,6 @@
[package]
name = "bot-database"
version = "0.1.0"
version = "0.3.0"
authors = ["trivernis <trivernis@protonmail.com>"]
edition = "2018"

@ -0,0 +1,2 @@
-- This file should undo anything in `up.sql`
DROP TABLE gifs;

@ -0,0 +1,8 @@
-- Your SQL goes here
CREATE TABLE gifs (
id BIGSERIAL PRIMARY KEY ,
category VARCHAR(128),
name VARCHAR(128),
url VARCHAR(128) NOT NULL,
UNIQUE (category, name)
)

@ -0,0 +1,2 @@
-- This file should undo anything in `up.sql`
DROP TABLE statistics;

@ -0,0 +1,9 @@
-- Your SQL goes here
CREATE TABLE statistics (
id BIGSERIAL PRIMARY KEY,
version VARCHAR(32) NOT NULL,
command VARCHAR(255) NOT NULL,
executed_at TIMESTAMP NOT NULL,
success BOOLEAN NOT NULL DEFAULT TRUE,
error_msg TEXT
)

@ -2,11 +2,13 @@ use crate::error::DatabaseResult;
use crate::models::*;
use crate::schema::*;
use crate::PoolConnection;
use diesel::dsl::count;
use diesel::prelude::*;
use diesel::{delete, insert_into};
use std::any;
use std::fmt::Debug;
use std::str::FromStr;
use std::time::SystemTime;
use tokio_diesel::*;
#[derive(Clone)]
@ -152,4 +154,88 @@ impl Database {
Ok(())
}
/// Returns a list of all gifs in the database
pub async fn get_all_gifs(&self) -> DatabaseResult<Vec<Gif>> {
use gifs::dsl;
log::debug!("Loading all gifs from the database");
let gifs: Vec<Gif> = dsl::gifs.load_async::<Gif>(&self.pool).await?;
Ok(gifs)
}
/// Returns a list of gifs by assigned category
pub async fn get_gifs_by_category(&self, category: &str) -> DatabaseResult<Vec<Gif>> {
use gifs::dsl;
log::debug!("Searching for gifs in category '{}'", category);
let gifs: Vec<Gif> = dsl::gifs
.filter(dsl::category.eq(category))
.load_async::<Gif>(&self.pool)
.await?;
Ok(gifs)
}
/// Adds a gif to the database
pub async fn add_gif(
&self,
url: &str,
category: Option<String>,
name: Option<String>,
) -> DatabaseResult<()> {
use gifs::dsl;
log::debug!(
"Inserting gif with url '{}' and name {:?} and category {:?}",
url,
name,
category
);
insert_into(dsl::gifs)
.values(GifInsert {
url: url.to_string(),
name,
category,
})
.execute_async(&self.pool)
.await?;
Ok(())
}
/// Adds a command statistic to the database
pub async fn add_statistic(
&self,
version: &str,
command: &str,
executed_at: SystemTime,
success: bool,
error_msg: Option<String>,
) -> DatabaseResult<()> {
use statistics::dsl;
log::trace!("Adding statistic to database");
insert_into(dsl::statistics)
.values(StatisticInsert {
version: version.to_string(),
command: command.to_string(),
executed_at,
success,
error_msg,
})
.execute_async(&self.pool)
.await?;
Ok(())
}
/// Returns the total number of commands executed
pub async fn get_total_commands_statistic(&self) -> DatabaseResult<u64> {
use statistics::dsl;
log::trace!("Querying total number of commands");
let total_count: i64 = dsl::statistics
.select(count(dsl::id))
.first_async::<i64>(&self.pool)
.await?;
Ok(total_count as u64)
}
}

@ -21,4 +21,13 @@ pub enum DatabaseError {
#[error("AsyncError: {0}")]
AsyncError(#[from] tokio_diesel::AsyncError),
#[error("{0}")]
Msg(String),
}
impl From<&str> for DatabaseError {
fn from(s: &str) -> Self {
Self::Msg(s.to_string())
}
}

@ -1,4 +1,5 @@
use crate::schema::*;
use std::time::SystemTime;
#[derive(Queryable, Debug)]
pub struct GuildSetting {
@ -29,3 +30,29 @@ pub struct GuildPlaylistInsert {
pub name: String,
pub url: String,
}
#[derive(Queryable, Debug, Clone)]
pub struct Gif {
pub id: i64,
pub category: Option<String>,
pub name: Option<String>,
pub url: String,
}
#[derive(Insertable, Debug)]
#[table_name = "gifs"]
pub struct GifInsert {
pub category: Option<String>,
pub name: Option<String>,
pub url: String,
}
#[derive(Insertable, Debug)]
#[table_name = "statistics"]
pub struct StatisticInsert {
pub version: String,
pub command: String,
pub executed_at: SystemTime,
pub success: bool,
pub error_msg: Option<String>,
}

@ -1,3 +1,12 @@
table! {
gifs (id) {
id -> Int8,
category -> Nullable<Varchar>,
name -> Nullable<Varchar>,
url -> Varchar,
}
}
table! {
guild_playlists (guild_id, name) {
guild_id -> Int8,
@ -14,7 +23,20 @@ table! {
}
}
table! {
statistics (id) {
id -> Int8,
version -> Varchar,
command -> Varchar,
executed_at -> Timestamp,
success -> Bool,
error_msg -> Nullable<Text>,
}
}
allow_tables_to_appear_in_same_query!(
gifs,
guild_playlists,
guild_settings,
statistics,
);

@ -12,11 +12,12 @@ use songbird::SerenityInit;
use crate::commands::*;
use crate::handler::Handler;
use crate::utils::context_data::{DatabaseContainer, Store, StoreData};
use crate::utils::context_data::{get_database_from_context, DatabaseContainer, Store, StoreData};
use crate::utils::error::{BotError, BotResult};
use bot_serenityutils::menu::EventDrivenMessageContainer;
use serenity::framework::standard::buckets::LimitedFor;
use std::sync::Arc;
use std::time::SystemTime;
use tokio::sync::Mutex;
pub async fn get_client() -> BotResult<Client> {
@ -58,6 +59,7 @@ pub async fn get_framework() -> StandardFramework {
.group(&MISC_GROUP)
.group(&MUSIC_GROUP)
.group(&SETTINGS_GROUP)
.group(&WEEB_GROUP)
.after(after_hook)
.before(before_hook)
.on_dispatch_error(dispatch_error)
@ -83,7 +85,9 @@ pub async fn get_framework() -> StandardFramework {
#[hook]
async fn after_hook(ctx: &Context, msg: &Message, cmd_name: &str, error: CommandResult) {
// Print out an error if it happened
let mut error_msg = None;
if let Err(why) = error {
error_msg = Some(why.to_string());
let _ = msg
.channel_id
.send_message(ctx, |m| {
@ -92,6 +96,16 @@ async fn after_hook(ctx: &Context, msg: &Message, cmd_name: &str, error: Command
.await;
log::warn!("Error in {}: {:?}", cmd_name, why);
}
let database = get_database_from_context(ctx).await;
let _ = database
.add_statistic(
env!("CARGO_PKG_VERSION"),
cmd_name,
SystemTime::now(),
error_msg.is_none(),
error_msg,
)
.await;
}
#[hook]

@ -0,0 +1,31 @@
use crate::utils::context_data::get_database_from_context;
use bot_coreutils::url;
use serenity::client::Context;
use serenity::framework::standard::macros::command;
use serenity::framework::standard::{Args, CommandResult};
use serenity::model::channel::Message;
#[command]
#[description("Simple ping test command")]
#[usage("<url> (<category>) (<name>)")]
#[bucket("general")]
#[aliases("add-gif", "addgif")]
#[min_args(1)]
#[max_args(3)]
#[owners_only]
async fn add_gif(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult {
let url = args.single::<String>()?;
if !url::is_valid(&url) {
msg.reply(ctx, "Invalid url").await?;
return Ok(());
}
let category = args.single_quoted::<String>().ok();
let name = args.single_quoted::<String>().ok();
let database = get_database_from_context(&ctx).await;
database.add_gif(&url, category, name).await?;
msg.reply(ctx, "Gif added to database").await?;
Ok(())
}

@ -0,0 +1,17 @@
use crate::messages::gifs::create_gifs_menu;
use crate::utils::context_data::get_database_from_context;
use serenity::client::Context;
use serenity::framework::standard::macros::command;
use serenity::framework::standard::CommandResult;
use serenity::model::channel::Message;
#[command]
#[description("Displays a list of all gifs used by the bot")]
#[bucket("general")]
async fn gifs(ctx: &Context, msg: &Message) -> CommandResult {
let database = get_database_from_context(ctx).await;
let gifs = database.get_all_gifs().await?;
create_gifs_menu(ctx, msg.channel_id, gifs).await?;
Ok(())
}

@ -1,26 +1,26 @@
use serenity::framework::standard::macros::group;
use about::ABOUT_COMMAND;
use pekofy::PEKOFY_COMMAND;
use add_gif::ADD_GIF_COMMAND;
use gifs::GIFS_COMMAND;
use ping::PING_COMMAND;
use qalc::QALC_COMMAND;
use sauce::SAUCE_COMMAND;
use shutdown::SHUTDOWN_COMMAND;
use stats::STATS_COMMAND;
use time::TIME_COMMAND;
use timezones::TIMEZONES_COMMAND;
mod about;
mod add_gif;
mod gifs;
pub(crate) mod help;
mod pekofy;
mod ping;
mod qalc;
mod sauce;
mod shutdown;
mod stats;
mod time;
mod timezones;
#[group]
#[commands(ping, stats, shutdown, pekofy, time, timezones, qalc, sauce, about)]
#[commands(ping, stats, shutdown, time, timezones, qalc, about, add_gif, gifs)]
pub struct Misc;

@ -9,6 +9,7 @@ use serenity::prelude::*;
use sysinfo::{ProcessExt, SystemExt};
use crate::commands::common::handle_autodelete;
use crate::utils::context_data::get_database_from_context;
const VERSION: &'static str = env!("CARGO_PKG_VERSION");
@ -18,6 +19,7 @@ const VERSION: &'static str = env!("CARGO_PKG_VERSION");
#[bucket("general")]
async fn stats(ctx: &Context, msg: &Message) -> CommandResult {
log::debug!("Reading system stats");
let database = get_database_from_context(ctx).await;
let mut system = sysinfo::System::new_all();
system.refresh_all();
let kernel_version = system.get_kernel_version().unwrap_or("n/a".to_string());
@ -32,14 +34,16 @@ async fn stats(ctx: &Context, msg: &Message) -> CommandResult {
let current_time_seconds = SystemTime::now().duration_since(UNIX_EPOCH).unwrap();
let uptime = current_time_seconds - Duration::from_secs(own_process.start_time());
let uptime = ChronoDuration::from_std(uptime).unwrap();
let total_commands_executed = database.get_total_commands_statistic().await?;
let discord_info = format!(
r#"
Version: {}
Owner: <@{}>
Guilds: {}
Times Used: {}
"#,
VERSION, bot_info.owner.id, guild_count
VERSION, bot_info.owner.id, guild_count, total_commands_executed
);
log::trace!("Discord info {}", discord_info);

@ -3,9 +3,11 @@ pub use misc::help::HELP;
pub use misc::MISC_GROUP;
pub use music::MUSIC_GROUP;
pub use settings::SETTINGS_GROUP;
pub use weeb::WEEB_GROUP;
mod common;
pub(crate) mod minecraft;
pub(crate) mod misc;
pub(crate) mod music;
pub(crate) mod settings;
pub(crate) mod weeb;

@ -0,0 +1,26 @@
use crate::utils::context_data::get_database_from_context;
use crate::utils::error::BotError;
use rand::prelude::IteratorRandom;
use serenity::client::Context;
use serenity::framework::standard::macros::command;
use serenity::framework::standard::CommandResult;
use serenity::model::channel::Message;
static GIF_CATEGORY: &str = "matsuri";
#[command]
#[description("Posts a random matsuri gif")]
#[usage("")]
#[bucket("general")]
async fn matsuri(ctx: &Context, msg: &Message) -> CommandResult {
let database = get_database_from_context(ctx).await;
let gifs = database.get_gifs_by_category(GIF_CATEGORY).await?;
let gif = gifs
.into_iter()
.choose(&mut rand::thread_rng())
.ok_or(BotError::from("No gifs found."))?;
msg.channel_id.say(ctx, gif.url).await?;
Ok(())
}

@ -0,0 +1,13 @@
use serenity::framework::standard::macros::group;
mod matsuri;
mod pekofy;
mod sauce;
use matsuri::MATSURI_COMMAND;
use pekofy::PEKOFY_COMMAND;
use sauce::SAUCE_COMMAND;
#[group]
#[commands(pekofy, sauce, matsuri)]
pub struct Weeb;

@ -4,7 +4,10 @@ use serenity::framework::standard::{Args, CommandError, CommandResult};
use serenity::model::channel::Message;
use serenity::{framework::standard::macros::command, prelude::*};
use crate::utils::context_data::get_database_from_context;
use crate::utils::error::{BotError, BotResult};
use crate::utils::get_previous_message_or_reply;
use bot_database::models::Gif;
// return a normal peko in most cases
static PEKOS: &[&str] = &[
@ -15,6 +18,7 @@ static PEKOS: &[&str] = &[
"🇵 🇪 🇰 🇴",
"p3k0",
];
static GIF_CATEGORY: &str = "pain-peko";
#[command]
#[description("Pekofy messages")]
@ -44,7 +48,7 @@ async fn pekofy(ctx: &Context, msg: &Message, args: Args) -> CommandResult {
alpha_lowercase.retain(|c| c.is_alphanumeric());
let pekofied: String = if alpha_lowercase == "pain" {
"https://tenor.com/view/pekora-usada-peko-hololive-died-gif-18114577".to_string()
random_pain_gif(ctx).await?.url
} else if PEKOS.contains(&&*alpha_lowercase) {
random_peko()
} else {
@ -108,3 +112,12 @@ fn random_peko() -> String {
"peko".to_string()
}
}
/// Chooses a random pain peko gif
async fn random_pain_gif(ctx: &Context) -> BotResult<Gif> {
let database = get_database_from_context(ctx).await;
let gifs = database.get_gifs_by_category(GIF_CATEGORY).await?;
gifs.into_iter()
.choose(&mut rand::thread_rng())
.ok_or(BotError::from("No gifs found"))
}

@ -0,0 +1,52 @@
use crate::utils::error::BotResult;
use bot_database::models::Gif;
use bot_serenityutils::menu::{MenuBuilder, Page};
use serenity::builder::CreateMessage;
use serenity::client::Context;
use serenity::model::id::ChannelId;
use std::time::Duration;
/// Creates a new gifs embed
pub async fn create_gifs_menu(
ctx: &Context,
channel_id: ChannelId,
gifs: Vec<Gif>,
) -> BotResult<()> {
let total_pages = (gifs.len() as f32 / 10.0).ceil() as usize;
let pages: Vec<Page> = gifs
.chunks(10)
.enumerate()
.map(|(page, gifs)| create_gifs_page(page + 1, total_pages, gifs.to_vec()))
.collect();
MenuBuilder::new_paginator()
.timeout(Duration::from_secs(120))
.add_pages(pages)
.build(ctx, channel_id)
.await?;
Ok(())
}
/// Creates a new gif page
pub fn create_gifs_page(page: usize, total_pages: usize, gifs: Vec<Gif>) -> Page<'static> {
let mut message = CreateMessage::default();
let description_lines: Vec<String> = gifs
.into_iter()
.map(|g| {
format!(
"{} - {} - [Source]({})",
g.category.unwrap_or("*N/A*".to_string()),
g.name.unwrap_or("*N/A*".to_string()),
g.url
)
})
.collect();
message.embed(|e| {
e.title("Gifs")
.description(description_lines.join("\n"))
.footer(|f| f.text(format!("Page {} of {}", page, total_pages)))
});
Page::new_static(message)
}

@ -1,2 +1,3 @@
pub mod gifs;
pub mod music;
pub mod sauce;

@ -14,6 +14,7 @@ use bot_serenityutils::menu::{Menu, MenuBuilder, Page};
use serenity::builder::CreateMessage;
use serenity::client::Context;
use serenity::model::channel::Reaction;
use std::env;
use std::time::Duration;
use tokio::sync::{Mutex, RwLock};
@ -98,7 +99,13 @@ fn create_now_playing_embed<'a>(
meta.title.clone().unwrap(),
meta.source_url.clone().unwrap(),
meta.artist.clone().unwrap()
));
))
.footer(|f| {
f.text(format!(
"Use {}play to add a song to the queue",
env::var("BOT_PREFIX").unwrap()
))
});
if let Some(thumb) = meta.thumbnail.clone() {
embed = embed.thumbnail(thumb);

Loading…
Cancel
Save