summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Cargo.lock33
-rw-r--r--Cargo.toml5
-rw-r--r--migrations/00000000000000_diesel_initial_setup/down.sql6
-rw-r--r--migrations/00000000000000_diesel_initial_setup/up.sql36
-rw-r--r--migrations/2018-02-15-031841_create_memes/down.sql3
-rw-r--r--migrations/2018-02-15-031841_create_memes/up.sql14
-rw-r--r--src/commands.rs765
-rw-r--r--src/db/mod.rs63
-rw-r--r--src/db/models.rs18
-rw-r--r--src/db/schema.rs21
-rw-r--r--src/main.rs10
-rw-r--r--src/util.rs68
12 files changed, 626 insertions, 416 deletions
diff --git a/Cargo.lock b/Cargo.lock
index c77d10b..97af1ff 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -129,6 +129,26 @@ dependencies = [
]
[[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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -556,6 +576,14 @@ 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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -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 <avaglir@gmail.com>"]
+[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<Mutex<ClientVoiceManager>>;
-}
-
-impl VoiceManager {
- pub fn register(c: &mut Client) {
- let mut data = c.data.lock();
- data.insert::<VoiceManager>(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<PlayArgs>,
- playing: Option<CurrentItem>,
-}
-
-impl Key for PlayQueue {
- type Value = Arc<RwLock<PlayQueue>>;
-}
-
-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::<PlayQueue>(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::<u64>("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::<String>() {
- 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::<PlayQueue>().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::<PlayQueue>().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::<PlayQueue>().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::<VoiceManager>().cloned().unwrap();
- let mut manager = mgr_lock.lock();
-
- let mut queue_lock = data.get::<PlayQueue>().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::<VoiceManager>().cloned().unwrap();
- let mut manager = mgr_lock.lock();
-
- let mut queue_lock = data.get::<PlayQueue>().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::<PlayQueue>().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::<VoiceManager>().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::<VoiceManager>().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<Mutex<ClientVoiceManager>>;
+}
+
+impl VoiceManager {
+ pub fn register(c: &mut Client) {
+ let mut data = c.data.lock();
+ data.insert::<VoiceManager>(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<PlayArgs>,
+ playing: Option<CurrentItem>,
+}
+
+impl Key for PlayQueue {
+ type Value = Arc<RwLock<PlayQueue>>;
+}
+
+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::<PlayQueue>(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::<u64>("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::<String>() {
+ 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::<PlayQueue>().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::<PlayQueue>().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::<PlayQueue>().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::<VoiceManager>().cloned().unwrap();
+ let mut manager = mgr_lock.lock();
+
+ let mut queue_lock = data.get::<PlayQueue>().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::<VoiceManager>().cloned().unwrap();
+ let mut manager = mgr_lock.lock();
+
+ let mut queue_lock = data.get::<PlayQueue>().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::<PlayQueue>().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::<VoiceManager>().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::<VoiceManager>().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<PgConnection> {
+ let database_url = env::var("DATABASE_URL")?;
+ PgConnection::establish(&database_url).map_err(Error::from)
+}
+
+pub fn find_text(search: String) -> Result<TextMeme> {
+ 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::<TextMeme>(&conn)
+ .map_err(Error::from)
+}
+
+pub fn find_audio(search: String) -> Result<AudioMeme> {
+ use self::schema::audio_memes::dsl::*;
+
+ let format_search = format!("%{}%", search);
+
+ let conn = connection()?;
+ audio_memes
+ .filter(title.ilike(format_search))
+ .limit(1)
+ .first::<AudioMeme>(&conn)
+ .map_err(Error::from)
+}
+
+pub fn rand_audio() -> Result<AudioMeme> {
+ use self::schema::audio_memes::dsl::*;
+
+ let conn = connection()?;
+ audio_memes
+ .order(random.desc())
+ .first::<AudioMeme>(&conn)
+ .map_err(Error::from)
+}
+
+pub fn rand_text() -> Result<TextMeme> {
+ use self::schema::text_memes::dsl::*;
+
+ let conn = connection()?;
+ text_memes
+ .order(random.desc())
+ .first::<TextMeme>(&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<T: FromStr>(s: &str) -> T {
- env::var(s).expect(&format!("missing env var {}", s))
- .parse::<T>().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<T: FromStr>(s: &str) -> T {
+ env::var(s).expect(&format!("missing env var {}", s))
+ .parse::<T>().unwrap_or_else(|_| panic!(format!("bad format for {}", s)))
+}