From d410d75ca967d476784eaca22d6e49f21ca318dd Mon Sep 17 00:00:00 2001 From: Nathan Perry Date: Thu, 15 Feb 2018 00:03:32 -0500 Subject: start adding in diesel --- Cargo.lock | 33 + Cargo.toml | 5 + .../00000000000000_diesel_initial_setup/down.sql | 6 + .../00000000000000_diesel_initial_setup/up.sql | 36 + migrations/2018-02-15-031841_create_memes/down.sql | 3 + migrations/2018-02-15-031841_create_memes/up.sql | 14 + src/commands.rs | 765 +++++++++++---------- src/db/mod.rs | 63 ++ src/db/models.rs | 18 + src/db/schema.rs | 21 + src/main.rs | 10 + src/util.rs | 68 +- 12 files changed, 626 insertions(+), 416 deletions(-) create mode 100644 migrations/00000000000000_diesel_initial_setup/down.sql create mode 100644 migrations/00000000000000_diesel_initial_setup/up.sql create mode 100644 migrations/2018-02-15-031841_create_memes/down.sql create mode 100644 migrations/2018-02-15-031841_create_memes/up.sql create mode 100644 src/db/mod.rs create mode 100644 src/db/models.rs create mode 100644 src/db/schema.rs diff --git a/Cargo.lock b/Cargo.lock index c77d10b..97af1ff 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -128,6 +128,26 @@ dependencies = [ "syn 0.11.11 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "diesel" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "bitflags 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", + "byteorder 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)", + "diesel_derives 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "pq-sys 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "diesel_derives" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "quote 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)", + "syn 0.11.11 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "dotenv" version = "0.10.1" @@ -555,6 +575,14 @@ name = "pkg-config" version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "pq-sys" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "vcpkg 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "quote" version = "0.3.15" @@ -789,7 +817,9 @@ dependencies = [ name = "thulani" version = "0.1.0" dependencies = [ + "cfg-if 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", "chrono 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "diesel 1.1.1 (registry+https://github.com/rust-lang/crates.io-index)", "dotenv 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)", "error-chain 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)", "fern 0.5.3 (registry+https://github.com/rust-lang/crates.io-index)", @@ -955,6 +985,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum core-foundation 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "25bfd746d203017f7d5cbd31ee5d8e17f94b6521c7af77ece6c9e4b2d4b16c67" "checksum core-foundation-sys 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "065a5d7ffdcbc8fa145d6f0746f3555025b9097a9e9cda59f7467abae670c78d" "checksum derive-error-chain 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)" = "3c9ca9ade651388daad7c993f005d0d20c4f6fe78c1cdc93e95f161c6f5ede4a" +"checksum diesel 1.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "925325c57038f2f14c0413bdf6a92ca72acff644959d0a1a9ebf8d19be7e9c01" +"checksum diesel_derives 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "28e2b2605ac6a3b9a586383f5f8b2b5f1108f07a421ade965b266289d2805e79" "checksum dotenv 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)" = "d6f0e2bb24d163428d8031d3ebd2d2bd903ad933205a97d0f18c7c1aade380f3" "checksum dtoa 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "09c3753c3db574d215cba4ea76018483895d7bff25a31b49ba45db21c48e50ab" "checksum error-chain 0.10.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d9435d864e017c3c6afeac1654189b06cdb491cf2ff73dbf0d73b0f292f42ff8" @@ -1005,6 +1037,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum phf_generator 0.7.21 (registry+https://github.com/rust-lang/crates.io-index)" = "6b07ffcc532ccc85e3afc45865469bf5d9e4ef5bfcf9622e3cfe80c2d275ec03" "checksum phf_shared 0.7.21 (registry+https://github.com/rust-lang/crates.io-index)" = "07e24b0ca9643bdecd0632f2b3da6b1b89bbb0030e0b992afc1113b23a7bc2f2" "checksum pkg-config 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)" = "3a8b4c6b8165cd1a1cd4b9b120978131389f64bdaf456435caa41e630edba903" +"checksum pq-sys 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)" = "4dfb5e575ef93a1b7b2a381d47ba7c5d4e4f73bff37cee932195de769aad9a54" "checksum quote 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)" = "7a6e920b65c65f10b2ae65c831a81a073a89edd28c7cce89475bff467ab4167a" "checksum rand 0.3.22 (registry+https://github.com/rust-lang/crates.io-index)" = "15a732abf9d20f0ad8eeb6f909bf6868722d9a06e1e50802b6a70351f40b4eb1" "checksum rand 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "eba5f8cb59cc50ed56be8880a5c7b496bfd9bd26394e176bc67884094145c2c5" diff --git a/Cargo.toml b/Cargo.toml index 0f9a5d0..8118a0a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,6 +3,9 @@ name = "thulani" version = "0.1.0" authors = ["Nathan Perry "] +[features] +default = ["diesel"] + [dependencies] lazy_static = "1.0" error-chain = "0.11.0" @@ -12,6 +15,8 @@ url = "1.6.0" dotenv = "0.10.1" chrono = "0.4" fern = { version = "0.5", features = ["colored"] } +diesel = { version = "1.0.0", features = ["postgres"], optional = true } +cfg-if = "0.1" [dependencies.serenity] version = "0.5" diff --git a/migrations/00000000000000_diesel_initial_setup/down.sql b/migrations/00000000000000_diesel_initial_setup/down.sql new file mode 100644 index 0000000..a9f5260 --- /dev/null +++ b/migrations/00000000000000_diesel_initial_setup/down.sql @@ -0,0 +1,6 @@ +-- This file was automatically created by Diesel to setup helper functions +-- and other internal bookkeeping. This file is safe to edit, any future +-- changes will be added to existing projects as new migrations. + +DROP FUNCTION IF EXISTS diesel_manage_updated_at(_tbl regclass); +DROP FUNCTION IF EXISTS diesel_set_updated_at(); diff --git a/migrations/00000000000000_diesel_initial_setup/up.sql b/migrations/00000000000000_diesel_initial_setup/up.sql new file mode 100644 index 0000000..d68895b --- /dev/null +++ b/migrations/00000000000000_diesel_initial_setup/up.sql @@ -0,0 +1,36 @@ +-- This file was automatically created by Diesel to setup helper functions +-- and other internal bookkeeping. This file is safe to edit, any future +-- changes will be added to existing projects as new migrations. + + + + +-- Sets up a trigger for the given table to automatically set a column called +-- `updated_at` whenever the row is modified (unless `updated_at` was included +-- in the modified columns) +-- +-- # Example +-- +-- ```sql +-- CREATE TABLE users (id SERIAL PRIMARY KEY, updated_at TIMESTAMP NOT NULL DEFAULT NOW()); +-- +-- SELECT diesel_manage_updated_at('users'); +-- ``` +CREATE OR REPLACE FUNCTION diesel_manage_updated_at(_tbl regclass) RETURNS VOID AS $$ +BEGIN + EXECUTE format('CREATE TRIGGER set_updated_at BEFORE UPDATE ON %s + FOR EACH ROW EXECUTE PROCEDURE diesel_set_updated_at()', _tbl); +END; +$$ LANGUAGE plpgsql; + +CREATE OR REPLACE FUNCTION diesel_set_updated_at() RETURNS trigger AS $$ +BEGIN + IF ( + NEW IS DISTINCT FROM OLD AND + NEW.updated_at IS NOT DISTINCT FROM OLD.updated_at + ) THEN + NEW.updated_at := current_timestamp; + END IF; + RETURN NEW; +END; +$$ LANGUAGE plpgsql; diff --git a/migrations/2018-02-15-031841_create_memes/down.sql b/migrations/2018-02-15-031841_create_memes/down.sql new file mode 100644 index 0000000..8de6d95 --- /dev/null +++ b/migrations/2018-02-15-031841_create_memes/down.sql @@ -0,0 +1,3 @@ +DROP TABLE text_memes; +DROP TABLE image_memes; +DROP TABLE audio_memes; diff --git a/migrations/2018-02-15-031841_create_memes/up.sql b/migrations/2018-02-15-031841_create_memes/up.sql new file mode 100644 index 0000000..38097e7 --- /dev/null +++ b/migrations/2018-02-15-031841_create_memes/up.sql @@ -0,0 +1,14 @@ +CREATE TABLE text_memes ( + id SERIAL PRIMARY KEY, + title varchar UNIQUE NOT NULL, + content TEXT NOT NULL, + pic_related varchar NULL, + UNIQUE(content, pic_related) +); + +CREATE TABLE audio_memes ( + id SERIAL PRIMARY KEY, + title varchar UNIQUE NOT NULL, + link varchar UNIQUE NOT NULL +); + diff --git a/src/commands.rs b/src/commands.rs index 61621f4..7023ebd 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -1,383 +1,384 @@ -use std::sync::{Arc, RwLock}; -use std::collections::VecDeque; -use std::thread; -use std::time::Duration; - -use serenity::prelude::*; -use serenity::client::bridge::voice::ClientVoiceManager; -use serenity::framework::StandardFramework; -use serenity::model::id::ChannelId; -use serenity::voice::{LockedAudio, ytdl}; - -use typemap::Key; - -use {Result, TARGET_GUILD_ID, must_env_lookup}; - -pub struct VoiceManager; - -impl Key for VoiceManager { - type Value = Arc>; -} - -impl VoiceManager { - pub fn register(c: &mut Client) { - let mut data = c.data.lock(); - data.insert::(Arc::clone(&c.voice_manager)); - } -} - -#[derive(Clone, Debug)] -struct PlayArgs { - url: String, - initiator: String, - sender_channel: ChannelId, -} - -#[derive(Clone)] -struct CurrentItem { - init_args: PlayArgs, - audio: LockedAudio, -} - -#[derive(Clone)] -pub struct PlayQueue { - queue: VecDeque, - playing: Option, -} - -impl Key for PlayQueue { - type Value = Arc>; -} - -impl PlayQueue { - fn new() -> Self { - PlayQueue { - queue: VecDeque::new(), - playing: None, - } - } - - pub fn register(c: &mut Client) { - let voice_manager = Arc::clone(&c.voice_manager); - - let mut data = c.data.lock(); - let queue = Arc::new(RwLock::new(PlayQueue::new())); - - data.insert::(Arc::clone(&queue)); - - thread::spawn(move || { - let queue_lck = Arc::clone(&queue); - let voice_manager = voice_manager; - - loop { - thread::sleep(Duration::from_millis(250)); - let (queue_is_empty, queue_has_playing) = { - let queue = queue_lck.read().unwrap(); - - let allow_continue = queue.playing.clone().map_or(false, |x| !x.audio.lock().finished); - - if allow_continue { - continue; - } - - (queue.queue.is_empty(), queue.playing.is_some()) - }; - - if queue_is_empty { - if queue_has_playing { - let mut queue = queue_lck.write().unwrap(); - - assert!({ - let audio_lck = queue.playing.clone().unwrap().audio; - let audio = audio_lck.lock(); - audio.finished - }); - - queue.playing = None; - - let mut manager = voice_manager.lock(); - manager.leave(*TARGET_GUILD_ID); - debug!("disconnected due to inactivity"); - } - continue; - } - - let mut queue = queue_lck.write().unwrap(); - let item = queue.queue.pop_front().unwrap(); - - trace!("checking ytdl for: {}", item.url); - - let src = match ytdl(&item.url) { - Ok(src) => src, - Err(e) => { - error!("bad link: {}; {:?}", &item.url, e); - let _ = send(item.sender_channel, &format!("what the fuck"), false); - continue; - } - }; - - trace!("got ytdl item for {}", item.url); - - let mut manager = voice_manager.lock(); - let handler = manager.join(*TARGET_GUILD_ID, must_env_lookup::("VOICE_CHANNEL")); - - match handler { - Some(handler) => { - let audio = handler.play_only(src); - queue.playing = Some(CurrentItem { - init_args: item, - audio, - }); - debug!("playing new song"); - }, - None => { - error!("couldn't join channel"); - let _ = send(item.sender_channel, "something happened somewhere somehow.", false); - } - } - } - }); - } -} - -fn send(channel: ChannelId, text: &str, tts: bool) -> Result<()> { - channel.send_message(|m| m.content(text).tts(tts))?; - Ok(()) -} - -pub fn register_commands(f: StandardFramework) -> StandardFramework { - f - .command("skip", |c| c - .desc("skip the rest of the current request") - .guild_only(true) - .cmd(skip)) - .command("pause", |c| c - .desc("pause playback (currently broken)") - .guild_only(true) - .cmd(pause)) - .command("resume", |c| c - .desc("resume playing (currently broken)") - .guild_only(true) - .cmd(resume)) - .command("list", |c| c - .known_as("queue") - .desc("list playing and queued requests") - .guild_only(true) - .cmd(list)) - .command("die", |c| c - .known_as("sudoku") - .desc("stop playing and empty the queue") - .guild_only(true) - .cmd(die)) - .command("meme", |c| c - .guild_only(true) - .cmd(meme)) - .command("mute", |c| c - .desc("mute thulani (playback continues)") - .guild_only(true) - .cmd(mute)) - .command("unmute", |c| c - .desc("unmute thulani") - .guild_only(true) - .cmd(unmute)) - .command("play", |c| c - .desc("queue a request") - .guild_only(true) - .cmd(play)) -} - -command!(play(ctx, msg, args) { - let url = match args.single::() { - Ok(url) => url, - Err(_) => { - send(msg.channel_id, "BAD LINK", msg.tts)?; - return Ok(()); - } - }; - - if !url.starts_with("http") { - send(msg.channel_id, "bAD LiNk", msg.tts)?; - return Ok(()); - } - - if url.contains("imgur") { - send(msg.channel_id, "IMGUR IS BAD, YOU TRASH CAN MAN", msg.tts)?; - return Ok(()); - } - - trace!("acquiring queue lock"); - - let mut queue_lock = ctx.data.lock().get::().cloned().unwrap(); - let mut play_queue = queue_lock.write().unwrap(); - - trace!("queue lock acquired"); - - play_queue.queue.push_back(PlayArgs{ - initiator: msg.author.name.clone(), - url, - sender_channel: msg.channel_id, - }); -}); - -command!(pause(ctx, msg) { - let mut queue_lock = ctx.data.lock().get::().cloned().unwrap(); - - let done = || send(msg.channel_id, "r u srs", msg.tts); - let playing = { - let play_queue = queue_lock.read().unwrap(); - - let current_item = match play_queue.playing { - Some(ref x) => x, - None => { - done()?; - return Ok(()); - }, - }; - - let audio = current_item.audio.lock(); - audio.playing - }; - - if !playing { - done()?; - return Ok(()); - } - - { - let queue = queue_lock.write().unwrap(); - let ref audio = queue.playing.clone().unwrap().audio; - audio.lock().pause(); - } -}); - -command!(resume(ctx, msg) { - let mut queue_lock = ctx.data.lock().get::().cloned().unwrap(); - - let done = || send(msg.channel_id, "r u srs", msg.tts); - let playing = { - let play_queue = queue_lock.read().unwrap(); - - let current_item = match play_queue.playing { - Some(ref x) => x, - None => { - done()?; - return Ok(()); - }, - }; - - let audio = current_item.audio.lock(); - audio.playing - }; - - if playing { - done()?; - return Ok(()); - } - - { - let queue = queue_lock.write().unwrap(); - let ref audio = queue.playing.clone().unwrap().audio; - audio.lock().play(); - } -}); - -command!(skip(ctx, _msg) { - let data = ctx.data.lock(); - - let mut mgr_lock = data.get::().cloned().unwrap(); - let mut manager = mgr_lock.lock(); - - let mut queue_lock = data.get::().cloned().unwrap(); - - if let Some(handler) = manager.get_mut(*TARGET_GUILD_ID) { - handler.stop(); - let mut play_queue = queue_lock.write().unwrap(); - play_queue.playing = None; - } else { - debug!("got skip with no handler attached"); - } -}); - -command!(die(ctx, msg) { - let data = ctx.data.lock(); - - let mut mgr_lock = data.get::().cloned().unwrap(); - let mut manager = mgr_lock.lock(); - - let mut queue_lock = data.get::().cloned().unwrap(); - - { - let mut play_queue = queue_lock.write().unwrap(); - - play_queue.playing = None; - play_queue.queue.clear(); - } - - if let Some(handler) = manager.get_mut(*TARGET_GUILD_ID) { - handler.stop(); - handler.leave(); - } else { - send(msg.channel_id, "YOU die", msg.tts)?; - debug!("got die with no handler attached"); - } -}); - -command!(list(ctx, msg) { - let mut queue_lock = ctx.data.lock().get::().cloned().unwrap(); - let mut play_queue = queue_lock.read().unwrap(); - - let channel_tmp = msg.channel().unwrap().guild().unwrap(); - let channel = channel_tmp.read(); - - match play_queue.playing { - Some(ref info) => { - let audio = info.audio.lock(); - let status = if audio.playing { "playing" } else { "paused:" }; - send(msg.channel_id, &format!("Currently {} `{}` ({})", status, info.init_args.url, info.init_args.initiator), msg.tts)?; - }, - None => { - debug!("`list` called with no items in queue"); - send(msg.channel_id, "Nothing is playing you meme", msg.tts)?; - return Ok(()); - }, - } - - play_queue.queue.iter().for_each(|info| { - channel.say(&format!("`{}` ({})", info.url, info.initiator)).unwrap(); - }); -}); - -command!(meme(_ctx, msg) { - send(msg.channel_id, "I am not yet capable of memeing", msg.tts)?; -}); - -command!(mute(ctx, _msg) { - let mut mgr_lock = ctx.data.lock().get::().cloned().unwrap(); - let mut manager = mgr_lock.lock(); - - manager.get_mut(*TARGET_GUILD_ID) - .map(|handler| { - if handler.self_mute { - trace!("Already muted.") - } else { - handler.mute(true); - trace!("Muted"); - } - }); -}); - -command!(unmute(ctx, msg) { - let mut mgr_lock = ctx.data.lock().get::().cloned().unwrap(); - let mut manager = mgr_lock.lock(); - - manager.get_mut(*TARGET_GUILD_ID) - .map(|handler| { - if !handler.self_mute { - trace!("Already unmuted.") - } else { - handler.mute(false); - trace!("Unmuted"); - let _ = send(msg.channel_id, "REEEEEEEEEEEEEE", msg.tts); - } - }); +use std::sync::{Arc, RwLock}; +use std::collections::VecDeque; +use std::thread; +use std::time::Duration; + +use serenity::prelude::*; +use serenity::client::bridge::voice::ClientVoiceManager; +use serenity::framework::StandardFramework; +use serenity::model::id::ChannelId; +use serenity::voice::{LockedAudio, ytdl}; + +use typemap::Key; + +use {Result, TARGET_GUILD_ID, must_env_lookup}; + +pub struct VoiceManager; + +impl Key for VoiceManager { + type Value = Arc>; +} + +impl VoiceManager { + pub fn register(c: &mut Client) { + let mut data = c.data.lock(); + data.insert::(Arc::clone(&c.voice_manager)); + } +} + +#[derive(Clone, Debug)] +struct PlayArgs { + url: String, + initiator: String, + sender_channel: ChannelId, +} + +#[derive(Clone)] +struct CurrentItem { + init_args: PlayArgs, + audio: LockedAudio, +} + +#[derive(Clone)] +pub struct PlayQueue { + queue: VecDeque, + playing: Option, +} + +impl Key for PlayQueue { + type Value = Arc>; +} + +impl PlayQueue { + fn new() -> Self { + PlayQueue { + queue: VecDeque::new(), + playing: None, + } + } + + pub fn register(c: &mut Client) { + let voice_manager = Arc::clone(&c.voice_manager); + + let mut data = c.data.lock(); + let queue = Arc::new(RwLock::new(PlayQueue::new())); + + data.insert::(Arc::clone(&queue)); + + thread::spawn(move || { + let queue_lck = Arc::clone(&queue); + let voice_manager = voice_manager; + + loop { + thread::sleep(Duration::from_millis(250)); + let (queue_is_empty, queue_has_playing) = { + let queue = queue_lck.read().unwrap(); + + let allow_continue = queue.playing.clone().map_or(false, |x| !x.audio.lock().finished); + + if allow_continue { + continue; + } + + (queue.queue.is_empty(), queue.playing.is_some()) + }; + + if queue_is_empty { + if queue_has_playing { + let mut queue = queue_lck.write().unwrap(); + + assert!({ + let audio_lck = queue.playing.clone().unwrap().audio; + let audio = audio_lck.lock(); + audio.finished + }); + + queue.playing = None; + + let mut manager = voice_manager.lock(); + manager.leave(*TARGET_GUILD_ID); + debug!("disconnected due to inactivity"); + } + continue; + } + + let mut queue = queue_lck.write().unwrap(); + let item = queue.queue.pop_front().unwrap(); + + trace!("checking ytdl for: {}", item.url); + + let src = match ytdl(&item.url) { + Ok(src) => src, + Err(e) => { + error!("bad link: {}; {:?}", &item.url, e); + let _ = send(item.sender_channel, &format!("what the fuck"), false); + continue; + } + }; + + trace!("got ytdl item for {}", item.url); + + let mut manager = voice_manager.lock(); + let handler = manager.join(*TARGET_GUILD_ID, must_env_lookup::("VOICE_CHANNEL")); + + match handler { + Some(handler) => { + let audio = handler.play_only(src); + queue.playing = Some(CurrentItem { + init_args: item, + audio, + }); + debug!("playing new song"); + }, + None => { + error!("couldn't join channel"); + let _ = send(item.sender_channel, "something happened somewhere somehow.", false); + } + } + } + }); + } +} + +fn send(channel: ChannelId, text: &str, tts: bool) -> Result<()> { + channel.send_message(|m| m.content(text).tts(tts))?; + Ok(()) +} + +pub fn register_commands(f: StandardFramework) -> StandardFramework { + f + .command("skip", |c| c + .desc("skip the rest of the current request") + .guild_only(true) + .cmd(skip)) + .command("pause", |c| c + .desc("pause playback (currently broken)") + .guild_only(true) + .cmd(pause)) + .command("resume", |c| c + .desc("resume playing (currently broken)") + .guild_only(true) + .cmd(resume)) + .command("list", |c| c + .known_as("queue") + .desc("list playing and queued requests") + .guild_only(true) + .cmd(list)) + .command("die", |c| c + .batch_known_as(vec!["sudoku", "stop"]) + .desc("stop playing and empty the queue") + .guild_only(true) + .cmd(die)) + .command("meme", |c| c + .guild_only(true) + .help_available(false) + .cmd(meme)) + .command("mute", |c| c + .desc("mute thulani (playback continues)") + .guild_only(true) + .cmd(mute)) + .command("unmute", |c| c + .desc("unmute thulani") + .guild_only(true) + .cmd(unmute)) + .command("play", |c| c + .desc("queue a request") + .guild_only(true) + .cmd(play)) +} + +command!(play(ctx, msg, args) { + let url = match args.single::() { + Ok(url) => url, + Err(_) => { + send(msg.channel_id, "BAD LINK", msg.tts)?; + return Ok(()); + } + }; + + if !url.starts_with("http") { + send(msg.channel_id, "bAD LiNk", msg.tts)?; + return Ok(()); + } + + if url.contains("imgur") { + send(msg.channel_id, "IMGUR IS BAD, YOU TRASH CAN MAN", msg.tts)?; + return Ok(()); + } + + trace!("acquiring queue lock"); + + let mut queue_lock = ctx.data.lock().get::().cloned().unwrap(); + let mut play_queue = queue_lock.write().unwrap(); + + trace!("queue lock acquired"); + + play_queue.queue.push_back(PlayArgs{ + initiator: msg.author.name.clone(), + url, + sender_channel: msg.channel_id, + }); +}); + +command!(pause(ctx, msg) { + let mut queue_lock = ctx.data.lock().get::().cloned().unwrap(); + + let done = || send(msg.channel_id, "r u srs", msg.tts); + let playing = { + let play_queue = queue_lock.read().unwrap(); + + let current_item = match play_queue.playing { + Some(ref x) => x, + None => { + done()?; + return Ok(()); + }, + }; + + let audio = current_item.audio.lock(); + audio.playing + }; + + if !playing { + done()?; + return Ok(()); + } + + { + let queue = queue_lock.write().unwrap(); + let ref audio = queue.playing.clone().unwrap().audio; + audio.lock().pause(); + } +}); + +command!(resume(ctx, msg) { + let mut queue_lock = ctx.data.lock().get::().cloned().unwrap(); + + let done = || send(msg.channel_id, "r u srs", msg.tts); + let playing = { + let play_queue = queue_lock.read().unwrap(); + + let current_item = match play_queue.playing { + Some(ref x) => x, + None => { + done()?; + return Ok(()); + }, + }; + + let audio = current_item.audio.lock(); + audio.playing + }; + + if playing { + done()?; + return Ok(()); + } + + { + let queue = queue_lock.write().unwrap(); + let ref audio = queue.playing.clone().unwrap().audio; + audio.lock().play(); + } +}); + +command!(skip(ctx, _msg) { + let data = ctx.data.lock(); + + let mut mgr_lock = data.get::().cloned().unwrap(); + let mut manager = mgr_lock.lock(); + + let mut queue_lock = data.get::().cloned().unwrap(); + + if let Some(handler) = manager.get_mut(*TARGET_GUILD_ID) { + handler.stop(); + let mut play_queue = queue_lock.write().unwrap(); + play_queue.playing = None; + } else { + debug!("got skip with no handler attached"); + } +}); + +command!(die(ctx, msg) { + let data = ctx.data.lock(); + + let mut mgr_lock = data.get::().cloned().unwrap(); + let mut manager = mgr_lock.lock(); + + let mut queue_lock = data.get::().cloned().unwrap(); + + { + let mut play_queue = queue_lock.write().unwrap(); + + play_queue.playing = None; + play_queue.queue.clear(); + } + + if let Some(handler) = manager.get_mut(*TARGET_GUILD_ID) { + handler.stop(); + handler.leave(); + } else { + send(msg.channel_id, "YOU die", msg.tts)?; + debug!("got die with no handler attached"); + } +}); + +command!(list(ctx, msg) { + let mut queue_lock = ctx.data.lock().get::().cloned().unwrap(); + let mut play_queue = queue_lock.read().unwrap(); + + let channel_tmp = msg.channel().unwrap().guild().unwrap(); + let channel = channel_tmp.read(); + + match play_queue.playing { + Some(ref info) => { + let audio = info.audio.lock(); + let status = if audio.playing { "playing" } else { "paused:" }; + send(msg.channel_id, &format!("Currently {} `{}` ({})", status, info.init_args.url, info.init_args.initiator), msg.tts)?; + }, + None => { + debug!("`list` called with no items in queue"); + send(msg.channel_id, "Nothing is playing you meme", msg.tts)?; + return Ok(()); + }, + } + + play_queue.queue.iter().for_each(|info| { + channel.say(&format!("`{}` ({})", info.url, info.initiator)).unwrap(); + }); +}); + +command!(meme(_ctx, msg) { + send(msg.channel_id, "I am not yet capable of memeing", msg.tts)?; +}); + +command!(mute(ctx, _msg) { + let mut mgr_lock = ctx.data.lock().get::().cloned().unwrap(); + let mut manager = mgr_lock.lock(); + + manager.get_mut(*TARGET_GUILD_ID) + .map(|handler| { + if handler.self_mute { + trace!("Already muted.") + } else { + handler.mute(true); + trace!("Muted"); + } + }); +}); + +command!(unmute(ctx, msg) { + let mut mgr_lock = ctx.data.lock().get::().cloned().unwrap(); + let mut manager = mgr_lock.lock(); + + manager.get_mut(*TARGET_GUILD_ID) + .map(|handler| { + if !handler.self_mute { + trace!("Already unmuted.") + } else { + handler.mute(false); + trace!("Unmuted"); + let _ = send(msg.channel_id, "REEEEEEEEEEEEEE", msg.tts); + } + }); }); \ No newline at end of file diff --git a/src/db/mod.rs b/src/db/mod.rs new file mode 100644 index 0000000..8ce0011 --- /dev/null +++ b/src/db/mod.rs @@ -0,0 +1,63 @@ +use std::env; + +use diesel::prelude::*; + +use super::{Result, Error}; +pub use self::models::*; + +mod schema; +mod models; + +fn connection() -> Result { + let database_url = env::var("DATABASE_URL")?; + PgConnection::establish(&database_url).map_err(Error::from) +} + +pub fn find_text(search: String) -> Result { + use self::schema::text_memes::dsl::*; + + let format_search = format!("%{}%", search); + + let conn = connection()?; + text_memes + .filter(title.ilike(&format_search).or(content.ilike(&format_search))) + .limit(1) + .first::(&conn) + .map_err(Error::from) +} + +pub fn find_audio(search: String) -> Result { + use self::schema::audio_memes::dsl::*; + + let format_search = format!("%{}%", search); + + let conn = connection()?; + audio_memes + .filter(title.ilike(format_search)) + .limit(1) + .first::(&conn) + .map_err(Error::from) +} + +pub fn rand_audio() -> Result { + use self::schema::audio_memes::dsl::*; + + let conn = connection()?; + audio_memes + .order(random.desc()) + .first::(&conn) + .map_err(Error::from) +} + +pub fn rand_text() -> Result { + use self::schema::text_memes::dsl::*; + + let conn = connection()?; + text_memes + .order(random.desc()) + .first::(&conn) + .map_err(Error::from) +} + +use diesel::sql_types; +no_arg_sql_function!(random, sql_types::Double, "SQL random() function"); diff --git a/src/db/models.rs b/src/db/models.rs new file mode 100644 index 0000000..899fa1e --- /dev/null +++ b/src/db/models.rs @@ -0,0 +1,18 @@ +use super::schema::*; + +#[derive(Insertable, Queryable, Identifiable, AsChangeset)] +#[table_name="audio_memes"] +pub struct AudioMeme { + pub id: i32, + pub title: String, + pub link: String, +} + +#[derive(Insertable, Queryable, Identifiable, AsChangeset)] +#[table_name="text_memes"] +pub struct TextMeme { + pub id: i32, + pub title: String, + pub content: String, + pub pic_related: String, +} diff --git a/src/db/schema.rs b/src/db/schema.rs new file mode 100644 index 0000000..b29a1ca --- /dev/null +++ b/src/db/schema.rs @@ -0,0 +1,21 @@ +table! { + audio_memes (id) { + id -> Int4, + title -> Varchar, + link -> Varchar, + } +} + +table! { + text_memes (id) { + id -> Int4, + title -> Varchar, + content -> Text, + pic_related -> Varchar, + } +} + +allow_tables_to_appear_in_same_query!( + audio_memes, + text_memes, +); diff --git a/src/main.rs b/src/main.rs index cca9264..fb510a1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,6 +2,7 @@ #[macro_use] extern crate log; #[macro_use] extern crate error_chain; #[macro_use] extern crate lazy_static; +#[macro_use] extern crate cfg_if; extern crate dotenv; extern crate fern; @@ -9,6 +10,13 @@ extern crate typemap; extern crate url; extern crate chrono; +cfg_if! { + if #[cfg(feature = "diesel")] { + #[macro_use] extern crate diesel; + mod db; + } +} + mod commands; mod util; @@ -31,6 +39,8 @@ mod errors { foreign_links { Serenity(::serenity::Error); MissingVar(::std::env::VarError); + DieselConn(::diesel::ConnectionError) #[cfg(feature = "diesel")]; + Diesel(::diesel::result::Error) #[cfg(feature = "diesel")]; } } } diff --git a/src/util.rs b/src/util.rs index 4c59704..9b44bc2 100644 --- a/src/util.rs +++ b/src/util.rs @@ -1,34 +1,34 @@ -use std::env; -use std::str::FromStr; - -use serenity::model::permissions::Permissions; -use url::Url; - -lazy_static! { - static ref REQUIRED_PERMS: Permissions = Permissions::EMBED_LINKS | - Permissions::READ_MESSAGES | - Permissions::ADD_REACTIONS | - Permissions::SEND_MESSAGES | - Permissions::SEND_TTS_MESSAGES | - Permissions::MENTION_EVERYONE | - Permissions::USE_EXTERNAL_EMOJIS | - Permissions::CONNECT | - Permissions::SPEAK | - Permissions::CHANGE_NICKNAME | - Permissions::USE_VAD | - Permissions::ATTACH_FILES; -} - -lazy_static! { - pub static ref OAUTH_URL: Url = Url::parse( - &format!( - "https://discordapp.com/api/oauth2/authorize?scope=bot&permissions={}&client_id={}", - REQUIRED_PERMS.bits(), env::var("THULANI_CLIENT_ID").expect("client ID was missing. please specify THULANI_CLIENT_ID in env or .env."), - ) - ).unwrap(); -} - -pub fn must_env_lookup(s: &str) -> T { - env::var(s).expect(&format!("missing env var {}", s)) - .parse::().unwrap_or_else(|_| panic!(format!("bad format for {}", s))) -} +use std::env; +use std::str::FromStr; + +use serenity::model::permissions::Permissions; +use url::Url; + +lazy_static! { + static ref REQUIRED_PERMS: Permissions = Permissions::EMBED_LINKS | + Permissions::READ_MESSAGES | + Permissions::ADD_REACTIONS | + Permissions::SEND_MESSAGES | + Permissions::SEND_TTS_MESSAGES | + Permissions::MENTION_EVERYONE | + Permissions::USE_EXTERNAL_EMOJIS | + Permissions::CONNECT | + Permissions::SPEAK | + Permissions::CHANGE_NICKNAME | + Permissions::USE_VAD | + Permissions::ATTACH_FILES; +} + +lazy_static! { + pub static ref OAUTH_URL: Url = Url::parse( + &format!( + "https://discordapp.com/api/oauth2/authorize?scope=bot&permissions={}&client_id={}", + REQUIRED_PERMS.bits(), env::var("THULANI_CLIENT_ID").expect("client ID was missing. please specify THULANI_CLIENT_ID in env or .env."), + ) + ).unwrap(); +} + +pub fn must_env_lookup(s: &str) -> T { + env::var(s).expect(&format!("missing env var {}", s)) + .parse::().unwrap_or_else(|_| panic!(format!("bad format for {}", s))) +} -- cgit v1.3.1