//! This example adapts Twilight's [basic lavalink bot] to use Songbird as its voice driver. //! //! # Twilight-rs attribution //! ISC License (ISC) //! //! Copyright (c) 2019, 2020 (c) The Twilight Contributors //! //! Permission to use, copy, modify, and/or distribute this software for any purpose //! with or without fee is hereby granted, provided that the above copyright notice //! and this permission notice appear in all copies. //! //! THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH //! REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND //! FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, //! INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS //! OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER //! TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF //! THIS SOFTWARE. //! //! //! [basic lavalink bot]: https://github.com/twilight-rs/twilight/tree/trunk/lavalink/examples/basic-lavalink-bot use futures::StreamExt; use songbird::{ input::{Input, Restartable}, tracks::{PlayMode, TrackHandle}, Songbird, }; use std::{collections::HashMap, env, error::Error, future::Future, sync::Arc}; use tokio::sync::RwLock; use twilight_gateway::{Cluster, Event, Intents}; use twilight_http::Client as HttpClient; use twilight_model::{channel::Message, gateway::payload::MessageCreate, id::GuildId}; use twilight_standby::Standby; #[derive(Clone, Debug)] struct State { cluster: Cluster, http: HttpClient, trackdata: Arc>>, songbird: Arc, standby: Standby, } fn spawn( fut: impl Future>> + Send + 'static, ) { tokio::spawn(async move { if let Err(why) = fut.await { tracing::debug!("handler error: {:?}", why); } }); } #[tokio::main] async fn main() -> Result<(), Box> { // Initialize the tracing subscriber. tracing_subscriber::fmt::init(); let state = { let token = env::var("DISCORD_TOKEN")?; let http = HttpClient::new(&token); let user_id = http.current_user().await?.id; let cluster = Cluster::new(token, Intents::GUILD_MESSAGES | Intents::GUILD_VOICE_STATES).await?; let shard_count = cluster.shards().len(); let songbird = Songbird::twilight(cluster.clone(), shard_count as u64, user_id); cluster.up().await; State { cluster, http, trackdata: Default::default(), songbird, standby: Standby::new(), } }; let mut events = state.cluster.events(); while let Some(event) = events.next().await { state.standby.process(&event.1); state.songbird.process(&event.1).await; if let Event::MessageCreate(msg) = event.1 { if msg.guild_id.is_none() || !msg.content.starts_with('!') { continue; } match msg.content.splitn(2, ' ').next() { Some("!join") => spawn(join(msg.0, state.clone())), Some("!leave") => spawn(leave(msg.0, state.clone())), Some("!pause") => spawn(pause(msg.0, state.clone())), Some("!play") => spawn(play(msg.0, state.clone())), Some("!seek") => spawn(seek(msg.0, state.clone())), Some("!stop") => spawn(stop(msg.0, state.clone())), Some("!volume") => spawn(volume(msg.0, state.clone())), _ => continue, } } } Ok(()) } async fn join(msg: Message, state: State) -> Result<(), Box> { state .http .create_message(msg.channel_id) .content("What's the channel ID you want me to join?")? .await?; let author_id = msg.author.id; let msg = state .standby .wait_for_message(msg.channel_id, move |new_msg: &MessageCreate| { new_msg.author.id == author_id }) .await?; let channel_id = msg.content.parse::()?; let guild_id = msg.guild_id.ok_or("Can't join a non-guild channel.")?; let (_handle, success) = state.songbird.join(guild_id, channel_id).await; let content = match success { Ok(()) => format!("Joined <#{}>!", channel_id), Err(e) => format!("Failed to join <#{}>! Why: {:?}", channel_id, e), }; state .http .create_message(msg.channel_id) .content(content)? .await?; Ok(()) } async fn leave(msg: Message, state: State) -> Result<(), Box> { tracing::debug!( "leave command in channel {} by {}", msg.channel_id, msg.author.name ); let guild_id = msg.guild_id.unwrap(); state.songbird.leave(guild_id).await?; state .http .create_message(msg.channel_id) .content("Left the channel")? .await?; Ok(()) } async fn play(msg: Message, state: State) -> Result<(), Box> { tracing::debug!( "play command in channel {} by {}", msg.channel_id, msg.author.name ); state .http .create_message(msg.channel_id) .content("What's the URL of the audio to play?")? .await?; let author_id = msg.author.id; let msg = state .standby .wait_for_message(msg.channel_id, move |new_msg: &MessageCreate| { new_msg.author.id == author_id }) .await?; let guild_id = msg.guild_id.unwrap(); if let Ok(song) = Restartable::ytdl(msg.content.clone()).await { let input = Input::from(song); let content = format!( "Playing **{:?}** by **{:?}**", input .metadata .title .as_ref() .unwrap_or(&"".to_string()), input .metadata .artist .as_ref() .unwrap_or(&"".to_string()), ); state .http .create_message(msg.channel_id) .content(content)? .await?; if let Some(call_lock) = state.songbird.get(guild_id) { let mut call = call_lock.lock().await; let handle = call.play_source(input); let mut store = state.trackdata.write().await; store.insert(guild_id, handle); } } else { state .http .create_message(msg.channel_id) .content("Didn't find any results")? .await?; } Ok(()) } async fn pause(msg: Message, state: State) -> Result<(), Box> { tracing::debug!( "pause command in channel {} by {}", msg.channel_id, msg.author.name ); let guild_id = msg.guild_id.unwrap(); let store = state.trackdata.read().await; let content = if let Some(handle) = store.get(&guild_id) { let info = handle.get_info().await?; let paused = match info.playing { PlayMode::Play => { let _success = handle.pause(); false }, _ => { let _success = handle.play(); true }, }; let action = if paused { "Unpaused" } else { "Paused" }; format!("{} the track", action) } else { format!("No track to (un)pause!") }; state .http .create_message(msg.channel_id) .content(content)? .await?; Ok(()) } async fn seek(msg: Message, state: State) -> Result<(), Box> { tracing::debug!( "seek command in channel {} by {}", msg.channel_id, msg.author.name ); state .http .create_message(msg.channel_id) .content("Where in the track do you want to seek to (in seconds)?")? .await?; let author_id = msg.author.id; let msg = state .standby .wait_for_message(msg.channel_id, move |new_msg: &MessageCreate| { new_msg.author.id == author_id }) .await?; let guild_id = msg.guild_id.unwrap(); let position = msg.content.parse::()?; let store = state.trackdata.read().await; let content = if let Some(handle) = store.get(&guild_id) { if handle.is_seekable() { let _success = handle.seek_time(std::time::Duration::from_secs(position)); format!("Seeked to {}s", position) } else { format!("Track is not compatible with seeking!") } } else { format!("No track to seek over!") }; state .http .create_message(msg.channel_id) .content(content)? .await?; Ok(()) } async fn stop(msg: Message, state: State) -> Result<(), Box> { tracing::debug!( "stop command in channel {} by {}", msg.channel_id, msg.author.name ); let guild_id = msg.guild_id.unwrap(); if let Some(call_lock) = state.songbird.get(guild_id) { let mut call = call_lock.lock().await; let _ = call.stop(); } state .http .create_message(msg.channel_id) .content("Stopped the track")? .await?; Ok(()) } async fn volume(msg: Message, state: State) -> Result<(), Box> { tracing::debug!( "volume command in channel {} by {}", msg.channel_id, msg.author.name ); state .http .create_message(msg.channel_id) .content("What's the volume you want to set (0.0-10.0, 1.0 being the default)?")? .await?; let author_id = msg.author.id; let msg = state .standby .wait_for_message(msg.channel_id, move |new_msg: &MessageCreate| { new_msg.author.id == author_id }) .await?; let guild_id = msg.guild_id.unwrap(); let volume = msg.content.parse::()?; if !volume.is_finite() || volume > 10.0 || volume < 0.0 { state .http .create_message(msg.channel_id) .content("Invalid volume!")? .await?; return Ok(()); } let store = state.trackdata.read().await; let content = if let Some(handle) = store.get(&guild_id) { let _success = handle.set_volume(volume as f32); format!("Set the volume to {}", volume) } else { format!("No track to change volume!") }; state .http .create_message(msg.channel_id) .content(content)? .await?; Ok(()) }