aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/commands/db.rs5
-rw-r--r--src/commands/mod.rs95
-rw-r--r--src/commands/playback/mod.rs184
-rw-r--r--src/commands/playback/types.rs139
-rw-r--r--src/commands/sound.rs88
-rw-r--r--src/db/mod.rs63
-rw-r--r--src/db/models.rs65
-rw-r--r--src/db/schema.rs82
-rw-r--r--src/main.rs205
-rw-r--r--src/util.rs34
10 files changed, 960 insertions, 0 deletions
diff --git a/src/commands/db.rs b/src/commands/db.rs
new file mode 100644
index 0000000..4e8293a
--- /dev/null
+++ b/src/commands/db.rs
@@ -0,0 +1,5 @@
+use super::*;
+
+command!(meme(_ctx, msg) {
+ send(msg.channel_id, "I am not yet capable of memeing", msg.tts)?;
+});
diff --git a/src/commands/mod.rs b/src/commands/mod.rs
new file mode 100644
index 0000000..3a9cb66
--- /dev/null
+++ b/src/commands/mod.rs
@@ -0,0 +1,95 @@
+use {must_env_lookup, Result, TARGET_GUILD_ID};
+use serenity::framework::StandardFramework;
+use serenity::model::channel::Message;
+use serenity::model::id::ChannelId;
+use serenity::prelude::*;
+use serenity::voice::{LockedAudio, ytdl};
+use std::thread;
+use std::time::Duration;
+
+mod playback;
+mod sound;
+
+pub use self::sound::*;
+pub use self::playback::*;
+
+cfg_if! {
+ if #[cfg(feature = "diesel")] {
+ mod db;
+ pub use self::db::*;
+
+ fn register_db(f: StandardFramework) -> StandardFramework {
+ f
+ .command("meme", |c| c
+ .guild_only(true)
+ .help_available(false)
+ .cmd(meme))
+ }
+ } else {
+ fn register_db(f: StandardFramework) -> StandardFramework {
+ f
+ }
+ }
+}
+
+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 {
+ let f: StandardFramework = register_db(f);
+ 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("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("volume", |c| c
+ .desc("set playback volume")
+ .guild_only(true)
+ .cmd(volume))
+ .unrecognised_command(|ctx, msg, unrec| {
+ let url = match msg.content.split_whitespace().skip(1).next() {
+ Some(x) => x,
+ None => {
+ info!("received unrecognized command: {}", unrec);
+ let _ = send(msg.channel_id, "format your commands right. fuck you.", msg.tts);
+ return;
+ }
+ };
+
+ let _ = self::playback::_play(ctx, msg, &url);
+ })
+}
+
+
diff --git a/src/commands/playback/mod.rs b/src/commands/playback/mod.rs
new file mode 100644
index 0000000..1d4ee96
--- /dev/null
+++ b/src/commands/playback/mod.rs
@@ -0,0 +1,184 @@
+use super::*;
+
+pub use self::types::*;
+
+mod types;
+
+pub fn _play(ctx: &Context, msg: &Message, url: &str) -> Result<()> {
+ debug!("playing '{}'", url);
+ 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 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: url.to_owned(),
+ sender_channel: msg.channel_id,
+ });
+
+ Ok(())
+}
+
+command!(play(ctx, msg, args) {
+ if args.len() == 0 {
+ _resume(ctx, msg)?;
+ return Ok(());
+ }
+
+ let url = match args.single::<String>() {
+ Ok(url) => url,
+ Err(_) => {
+ send(msg.channel_id, "BAD LINK", msg.tts)?;
+ return Ok(());
+ }
+ };
+
+ _play(ctx, msg, &url)?;
+});
+
+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) {
+ _resume(ctx, msg)?;
+});
+
+fn _resume(ctx: &mut Context, msg: &Message) -> Result<()> {
+ let 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();
+ }
+
+ Ok(())
+}
+
+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();
+ });
+});
diff --git a/src/commands/playback/types.rs b/src/commands/playback/types.rs
new file mode 100644
index 0000000..41592ec
--- /dev/null
+++ b/src/commands/playback/types.rs
@@ -0,0 +1,139 @@
+use serenity::client::bridge::voice::ClientVoiceManager;
+use typemap::Key;
+use std::sync::{Arc, RwLock};
+use std::collections::VecDeque;
+use super::*;
+
+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)]
+pub struct PlayArgs {
+ pub url: String,
+ pub initiator: String,
+ pub sender_channel: ChannelId,
+}
+
+#[derive(Clone)]
+pub struct CurrentItem {
+ pub init_args: PlayArgs,
+ pub audio: LockedAudio,
+}
+
+#[derive(Clone)]
+pub struct PlayQueue {
+ pub queue: VecDeque<PlayArgs>,
+ pub playing: Option<CurrentItem>,
+ pub volume: f32,
+}
+
+impl Key for PlayQueue {
+ type Value = Arc<RwLock<PlayQueue>>;
+}
+
+impl PlayQueue {
+ pub fn new() -> Self {
+ PlayQueue {
+ queue: VecDeque::new(),
+ playing: None,
+ volume: DEFAULT_VOLUME,
+ }
+ }
+
+ 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 mut audio = handler.play_only(src);
+ {
+ audio.lock().volume(queue.volume);
+ }
+
+ 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);
+ }
+ }
+ }
+ });
+ }
+}
diff --git a/src/commands/sound.rs b/src/commands/sound.rs
new file mode 100644
index 0000000..6cac61d
--- /dev/null
+++ b/src/commands/sound.rs
@@ -0,0 +1,88 @@
+use super::*;
+
+pub const DEFAULT_VOLUME: f32 = 0.05;
+
+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);
+ }
+ });
+});
+
+command!(volume(ctx, msg, args) {
+ if args.len() == 0 {
+ let vol = {
+ let queue_lock = ctx.data.lock().get::<PlayQueue>().cloned().unwrap();
+ let mut play_queue = queue_lock.read().unwrap();
+ (play_queue.volume / DEFAULT_VOLUME * 100.0) as usize
+ };
+
+ send(msg.channel_id, &format!("Volume: {}/100", vol), msg.tts)?;
+ return Ok(());
+ }
+
+ let mut vol: usize = match args.single::<f32>() {
+ Ok(vol) if vol.is_nan() => {
+ send(msg.channel_id, "you're a fuck", msg.tts)?;
+ return Ok(());
+ },
+ Ok(vol) => vol as usize,
+ Err(_) => {
+ send(msg.channel_id, "???????", msg.tts)?;
+ return Ok(());
+ },
+ };
+
+ let mut vol: f32 = (vol as f32)/100.0; // force aliasing to reasonable values
+
+ if vol > 3.0 {
+ vol = 3.0;
+ }
+
+ if vol < 0.0 {
+ vol = 0.0;
+ }
+
+ let mut queue_lock = ctx.data.lock().get::<PlayQueue>().cloned().unwrap();
+
+ {
+ let mut play_queue = queue_lock.write().unwrap();
+ play_queue.volume = vol * DEFAULT_VOLUME;
+ }
+
+ {
+ let play_queue = queue_lock.read().unwrap();
+
+ let current_item = match play_queue.playing {
+ Some(ref x) => x,
+ None => return Ok(()),
+ };
+
+ let mut audio = current_item.audio.lock();
+ audio.volume(play_queue.volume);
+ };
+});
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..c06c41e
--- /dev/null
+++ b/src/db/models.rs
@@ -0,0 +1,65 @@
+use super::schema::*;
+use chrono::naive::NaiveDateTime;
+
+#[derive(Insertable, Queryable, Identifiable, AsChangeset, Debug, Associations)]
+#[belongs_to(Audio)]
+#[belongs_to(Image)]
+#[belongs_to(TextMeme)]
+#[belongs_to(ImageMeme)]
+#[belongs_to(TextMeme)]
+#[table_name="metadata"]
+pub struct Metadata {
+ pub id: i32,
+ pub created: NaiveDateTime,
+ pub created_by: i64,
+}
+
+#[derive(Insertable, Queryable, Identifiable, PartialEq, AsChangeset, Debug, Associations)]
+#[belongs_to(AudioMeme)]
+#[belongs_to(TextMeme)]
+#[table_name="audio"]
+pub struct Audio {
+ pub id: i32,
+ pub data: Vec<u8>,
+ pub metadata_id: i32,
+}
+
+#[derive(Insertable, Queryable, Identifiable, PartialEq, AsChangeset, Debug, Associations)]
+#[belongs_to(ImageMeme)]
+#[belongs_to(TextMeme)]
+#[table_name="images"]
+pub struct Image {
+ pub id: i32,
+ pub data: Vec<u8>,
+ pub metadata_id: i32,
+}
+
+#[derive(Insertable, Queryable, Identifiable, PartialEq, AsChangeset, Debug)]
+#[table_name="audio_memes"]
+pub struct AudioMeme {
+ pub id: i32,
+ pub title: String,
+ pub audio_id: i32,
+ pub metadata_id: i32,
+}
+
+#[derive(Insertable, Queryable, Identifiable, PartialEq, AsChangeset, Debug)]
+#[table_name="text_memes"]
+pub struct TextMeme {
+ pub id: i32,
+ pub title: String,
+ pub content: String,
+ pub image_id: Option<i32>,
+ pub audio_id: Option<i32>,
+ pub metadata_id: i32,
+}
+
+#[derive(Insertable, Queryable, Identifiable, PartialEq, AsChangeset, Debug, Associations)]
+#[belongs_to(Metadata)]
+#[table_name="audit_records"]
+pub struct AuditRecord {
+ pub id: i32,
+ pub updated: NaiveDateTime,
+ pub updated_by: i64,
+ pub metadata_id: i32,
+}
diff --git a/src/db/schema.rs b/src/db/schema.rs
new file mode 100644
index 0000000..40891a5
--- /dev/null
+++ b/src/db/schema.rs
@@ -0,0 +1,82 @@
+table! {
+ audio (id) {
+ id -> Int4,
+ data -> Bytea,
+ metadata_id -> Int4,
+ }
+}
+
+table! {
+ audio_memes (id) {
+ id -> Int4,
+ title -> Varchar,
+ audio_id -> Int4,
+ metadata_id -> Int4,
+ }
+}
+
+table! {
+ audit_records (id) {
+ id -> Int4,
+ updated -> Timestamp,
+ updated_by -> Int8,
+ metadata_id -> Int4,
+ }
+}
+
+table! {
+ image_memes (id) {
+ id -> Int4,
+ title -> Varchar,
+ image_id -> Int4,
+ metadata_id -> Int4,
+ }
+}
+
+table! {
+ images (id) {
+ id -> Int4,
+ data -> Bytea,
+ metadata_id -> Int4,
+ }
+}
+
+table! {
+ metadata (id) {
+ id -> Int4,
+ created -> Timestamp,
+ created_by -> Int8,
+ }
+}
+
+table! {
+ text_memes (id) {
+ id -> Int4,
+ title -> Varchar,
+ content -> Text,
+ image_id -> Nullable<Int4>,
+ audio_id -> Nullable<Int4>,
+ metadata_id -> Int4,
+ }
+}
+
+joinable!(audio -> metadata (metadata_id));
+joinable!(audio_memes -> audio (audio_id));
+joinable!(audio_memes -> metadata (metadata_id));
+joinable!(audit_records -> metadata (metadata_id));
+joinable!(image_memes -> images (image_id));
+joinable!(image_memes -> metadata (metadata_id));
+joinable!(images -> metadata (metadata_id));
+joinable!(text_memes -> audio (audio_id));
+joinable!(text_memes -> images (image_id));
+joinable!(text_memes -> metadata (metadata_id));
+
+allow_tables_to_appear_in_same_query!(
+ audio,
+ audio_memes,
+ audit_records,
+ image_memes,
+ images,
+ metadata,
+ text_memes,
+);
diff --git a/src/main.rs b/src/main.rs
new file mode 100644
index 0000000..fba574e
--- /dev/null
+++ b/src/main.rs
@@ -0,0 +1,205 @@
+#[macro_use] extern crate cfg_if;
+extern crate chrono;
+extern crate ctrlc;
+extern crate dotenv;
+#[macro_use] extern crate dotenv_codegen;
+#[macro_use] extern crate error_chain;
+extern crate fern;
+#[macro_use] extern crate lazy_static;
+#[macro_use] extern crate log;
+#[macro_use] extern crate serenity;
+extern crate typemap;
+extern crate url;
+
+use commands::register_commands;
+use dotenv::dotenv;
+use errors::*;
+use serenity::framework::standard::help_commands;
+use serenity::framework::StandardFramework;
+use serenity::model::gateway::Ready;
+use serenity::model::id::{GuildId, UserId};
+use serenity::prelude::*;
+use std::env;
+use std::thread;
+use std::time::{Duration, Instant};
+pub use util::*;
+cfg_if! {
+ if #[cfg(feature = "diesel")] {
+ #[macro_use] extern crate diesel;
+ mod db;
+ }
+}
+
+mod commands;
+mod util;
+
+mod errors {
+ error_chain! {
+ foreign_links {
+ Serenity(::serenity::Error);
+ MissingVar(::std::env::VarError);
+ DieselConn(::diesel::ConnectionError) #[cfg(feature = "diesel")];
+ Diesel(::diesel::result::Error) #[cfg(feature = "diesel")];
+ }
+ }
+}
+
+lazy_static! {
+ static ref TARGET_GUILD: u64 = dotenv!("TARGET_GUILD").parse().expect("unable to parse TARGET_GUILD as u64");
+ static ref TARGET_GUILD_ID: GuildId = GuildId(*TARGET_GUILD);
+}
+
+
+struct Handler;
+impl EventHandler for Handler {
+ fn ready(&self, _: Context, r: Ready) {
+ let guild = r.guilds.iter()
+ .find(|g| g.id().0 == *TARGET_GUILD);
+
+ if guild.is_none() {
+ info!("bot isn't in configured guild. join here: {:?}", OAUTH_URL.as_str());
+ }
+ }
+}
+
+fn run() -> Result<()> {
+ let token = &env::var("THULANI_TOKEN")?;
+ let mut client = Client::new(token, Handler)?;
+
+ commands::VoiceManager::register(&mut client);
+ commands::PlayQueue::register(&mut client);
+
+ let owner_id = must_env_lookup::<u64>("OWNER_ID");
+ let mut framework = StandardFramework::new()
+ .configure(|c| c
+ .allow_dm(false)
+ .allow_whitespace(true)
+ .prefixes(vec!["!thulani ", "!thulan ", "!thulando madando ", "!thulando "])
+ .ignore_bots(true)
+ .on_mention(false)
+ .owners(vec![UserId(owner_id)].into_iter().collect())
+ .case_insensitivity(true)
+ )
+ .before(|_ctx, message, cmd| {
+ let result = message.guild_id().map_or(false, |x| x.0 == *TARGET_GUILD);
+ debug!("got command '{}' from user '{}' ({}). accept: {}", cmd, message.author.name, message.author.id, result);
+
+ result
+ })
+ .after(|_ctx, _msg, _cmd, err| {
+ match err {
+ Ok(()) => {
+ trace!("command completed successfully");
+ },
+ Err(e) => {
+ error!("encountered error: {:?}", e);
+ }
+ }
+ })
+ .bucket("Standard", 1, 10, 3)
+ .customised_help(help_commands::with_embeds, |c| {
+ c
+ });
+
+ framework = register_commands(framework);
+ client.with_framework(framework);
+
+ let shard_manager = client.shard_manager.clone();
+ ctrlc::set_handler(move || {
+ info!("shutting down");
+ shard_manager.lock().shutdown_all();
+ }).expect("unable to create SIGINT/SIGTERM handlers");
+
+ client.start()?;
+
+ Ok(())
+}
+
+fn main() {
+ const BACKOFF_FACTOR: f64 = 2.0;
+ const MAX_BACKOFFS: usize = 3;
+ const BACKOFF_INIT: f64 = 100.0;
+
+ const MIN_RUN_DURATION: Duration = Duration::from_secs(120);
+
+ dotenv().ok();
+
+ use fern::colors::{Color, ColoredLevelConfig};
+ let colors = ColoredLevelConfig::new()
+ .info(Color::Green)
+ .debug(Color::BrightBlue)
+ .trace(Color::BrightMagenta);
+
+ fern::Dispatch::new()
+ .level_for("serenity::voice::connection", log::LevelFilter::Error)
+ .chain(fern::Dispatch::new()
+ .format(move |out, message, record| {
+ out.finish(format_args!(
+ "{} [{}] [{}] {}",
+ chrono::Local::now().format("%_m/%_d/%y %l:%M:%S%P"),
+ colors.color(record.level()),
+ record.target(),
+ message
+ ))
+ })
+ .level(log::LevelFilter::Warn)
+ .level_for("thulani", log::LevelFilter::Debug)
+ .chain(std::io::stdout())
+ )
+ .chain(fern::Dispatch::new()
+ .format(|out, message, record| {
+ out.finish(format_args!(
+ "{} [{}] [{}] {}",
+ chrono::Local::now().format("%_m/%_d/%y %l:%M:%S%P"),
+ record.level(),
+ record.target(),
+ message
+ ))
+ })
+
+ .level(log::LevelFilter::Info)
+ .level_for("thulani", log::LevelFilter::Trace)
+ .chain(fern::log_file("thulani.log").expect("problem creating log file"))
+ )
+ .apply()
+ .expect("error initializing logging");
+
+ let mut backoff_count: usize = 0;
+
+ loop {
+ let start = Instant::now();
+
+ info!("starting bot");
+ match run() {
+ Err(e) => {
+ error!("error encountered running client: {}", e);
+ e.iter().skip(1).for_each(|e| {
+ error!("caused by: {}", e);
+ });
+
+ if let Some(bt) = e.backtrace() {
+ error!("backtrace: {:?}", bt);
+ }
+ },
+ _ => {
+ // NOTE: we MUST have gotten here through SIGINT/SIGTERM handlers
+ ::std::process::exit(0);
+ }
+ }
+
+ if Instant::now() - start >= MIN_RUN_DURATION {
+ backoff_count = 0;
+ continue;
+ }
+
+ backoff_count += 1;
+ if backoff_count >= MAX_BACKOFFS {
+ panic!("restarted bot too many times");
+ }
+
+ let backoff_millis = (BACKOFF_INIT * BACKOFF_FACTOR.powi(backoff_count as i32)) as u64;
+ info!("bot died too quickly. backing off, retrying in {}ms.", backoff_millis);
+
+ thread::sleep(Duration::from_millis(backoff_millis));
+ }
+}
diff --git a/src/util.rs b/src/util.rs
new file mode 100644
index 0000000..1eb4b23
--- /dev/null
+++ b/src/util.rs
@@ -0,0 +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(), dotenv!("THULANI_CLIENT_ID"),
+ )
+ ).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)))
+}