use interactions instead of chat commands
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
2024-02-17 14:34:55 +01:00
parent abba563d23
commit 872464bd31
11 changed files with 369 additions and 136 deletions

View File

@@ -1,43 +1,84 @@
use std::{env, error::Error, num::NonZeroU64, time::Duration}; use std::{env, error::Error, num::NonZeroU64, time::Duration};
use tokio::time::sleep; use tokio::time::sleep;
use tracing::info; use tracing::{debug, info};
use twilight_model::{channel::Message, id::Id}; use twilight_model::{
application::interaction::Interaction,
channel::message::MessageFlags,
http::interaction::{InteractionResponse, InteractionResponseType},
id::Id,
};
use twilight_util::builder::InteractionResponseDataBuilder;
use crate::state::State; use crate::state::State;
pub(crate) async fn delete( pub(crate) async fn delete(
msg: Message, interaction: Interaction,
state: State, state: State,
count: i64,
) -> Result<(), Box<dyn Error + Send + Sync + 'static>> { ) -> Result<(), Box<dyn Error + Send + Sync + 'static>> {
debug!(
"delete command in guild {:?} in channel {:?} by {:?}",
interaction.guild_id,
interaction.channel,
interaction.author(),
);
let admin = env::var("ADMIN")?.parse::<u64>()?; let admin = env::var("ADMIN")?.parse::<u64>()?;
if msg.author.id != Id::from(NonZeroU64::new(admin).unwrap()) { if interaction.author_id() != Some(Id::from(NonZeroU64::new(admin).unwrap())) {
let interaction_response_data = InteractionResponseDataBuilder::new()
.content("You do not have permissions to delete messages.")
.flags(MessageFlags::EPHEMERAL)
.build();
let response = InteractionResponse {
kind: InteractionResponseType::ChannelMessageWithSource,
data: Some(interaction_response_data),
};
state
.http
.interaction(interaction.application_id)
.create_response(interaction.id, &interaction.token, &response)
.await?;
return Ok(()); return Ok(());
} }
let n = msg let Some(channel) = interaction.channel else {
.content
.split(' ')
.last()
.unwrap()
.parse::<u16>()
.unwrap_or(1);
if n > 100 {
return Ok(()); return Ok(());
} };
let Some(message_id) = channel.last_message_id else {
return Ok(());
};
let count = count.max(1).min(100) as u16;
let interaction_response_data = InteractionResponseDataBuilder::new()
.content(format!("Deleting {count} messages."))
.flags(MessageFlags::EPHEMERAL)
.build();
let response = InteractionResponse {
kind: InteractionResponseType::ChannelMessageWithSource,
data: Some(interaction_response_data),
};
state
.http
.interaction(interaction.application_id)
.create_response(interaction.id, &interaction.token, &response)
.await?;
let messages = state let messages = state
.http .http
.channel_messages(msg.channel_id) .channel_messages(channel.id)
.before(msg.id) .before(message_id.cast())
.limit(n)? .limit(count)?
.await? .await?
.model() .model()
.await?; .await?;
state.http.delete_message(msg.channel_id, msg.id).await?;
for message in messages { for message in messages {
info!("Delete message: {:?}: {:?}", message.author.name, message); debug!("Delete message: {:?}: {:?}", message.author.name, message);
state state.http.delete_message(channel.id, message.id).await?;
.http
.delete_message(msg.channel_id, message.id)
.await?;
sleep(Duration::from_secs(5)).await; sleep(Duration::from_secs(5)).await;
} }
Ok(()) Ok(())

View File

@@ -1,34 +1,81 @@
use std::{error::Error, num::NonZeroU64};
use twilight_model::channel::Message;
use crate::state::State; use crate::state::State;
use std::error::Error;
use tracing::debug;
use twilight_model::{
application::interaction::Interaction,
channel::message::MessageFlags,
http::interaction::{InteractionResponse, InteractionResponseType},
id::{
marker::{GuildMarker, UserMarker},
Id,
},
};
use twilight_util::builder::InteractionResponseDataBuilder;
pub(crate) async fn join( pub(crate) async fn join_channel(
msg: Message,
state: State, state: State,
guild_id: Id<GuildMarker>,
user_id: Id<UserMarker>,
) -> Result<(), Box<dyn Error + Send + Sync + 'static>> { ) -> Result<(), Box<dyn Error + Send + Sync + 'static>> {
let guild_id = msg.guild_id.ok_or("No guild id attached to the message.")?; debug!("join user {:?} in guild {:?}", user_id, guild_id);
let user_id = msg.author.id;
let channel_id = state let channel_id = state
.cache .cache
.voice_state(user_id, guild_id) .voice_state(user_id, guild_id)
.ok_or("Cannot get voice state for user")? .ok_or("Cannot get voice state for user")?
.channel_id(); .channel_id();
let channel_id =
NonZeroU64::new(channel_id.into()).ok_or("Joined voice channel must have nonzero ID.")?;
// join the voice channel // join the voice channel
state state
.songbird .songbird
.join(guild_id, channel_id) .join(guild_id.cast(), channel_id)
.await .await
.map_err(|e| format!("Could not join voice channel: {:?}", e))?; .map_err(|e| format!("Could not join voice channel: {:?}", e))?;
// signal that we are not listening // signal that we are not listening
if let Some(call_lock) = state.songbird.get(guild_id) { if let Some(call_lock) = state.songbird.get(guild_id.cast()) {
let mut call = call_lock.lock().await; let mut call = call_lock.lock().await;
call.deafen(true).await?; call.deafen(true).await?;
} }
Ok(()) Ok(())
} }
pub(crate) async fn join(
interaction: Interaction,
state: State,
) -> Result<(), Box<dyn Error + Send + Sync + 'static>> {
debug!(
"join command in guild {:?} in channel {:?} by {:?}",
interaction.guild_id,
interaction.channel,
interaction.author(),
);
let Some(guild_id) = interaction.guild_id else {
return Ok(());
};
let Some(author_id) = interaction.author_id() else {
return Ok(());
};
join_channel(state.clone(), guild_id, author_id).await?;
let interaction_response_data = InteractionResponseDataBuilder::new()
.content("Bin da Brudi!")
.flags(MessageFlags::EPHEMERAL)
.build();
let response = InteractionResponse {
kind: InteractionResponseType::ChannelMessageWithSource,
data: Some(interaction_response_data),
};
state
.http
.interaction(interaction.application_id)
.create_response(interaction.id, &interaction.token, &response)
.await?;
Ok(())
}

View File

@@ -1,17 +1,21 @@
use crate::state::State; use crate::state::State;
use std::error::Error; use std::error::Error;
use twilight_model::channel::Message; use twilight_model::application::interaction::Interaction;
pub(crate) async fn leave( pub(crate) async fn leave(
msg: Message, interaction: Interaction,
state: State, state: State,
) -> Result<(), Box<dyn Error + Send + Sync + 'static>> { ) -> Result<(), Box<dyn Error + Send + Sync + 'static>> {
tracing::debug!( tracing::debug!(
"leave command in channel {} by {}", "leave command n guild {:?} in channel {:?} by {:?}",
msg.channel_id, interaction.guild_id,
msg.author.name interaction.channel,
interaction.author(),
); );
let guild_id = msg.guild_id.unwrap();
let Some(guild_id) = interaction.guild_id else {
return Ok(());
};
state.songbird.leave(guild_id).await?; state.songbird.leave(guild_id).await?;
Ok(()) Ok(())
} }

View File

@@ -23,7 +23,7 @@ mod delete;
pub(crate) use delete::delete; pub(crate) use delete::delete;
use twilight_model::application::command::CommandType; use twilight_model::application::command::CommandType;
use twilight_util::builder::command::{CommandBuilder, StringBuilder}; use twilight_util::builder::command::{CommandBuilder, IntegerBuilder, StringBuilder};
pub(crate) fn get_chat_commands() -> Vec<twilight_model::application::command::Command> { pub(crate) fn get_chat_commands() -> Vec<twilight_model::application::command::Command> {
vec![ vec![
@@ -36,5 +36,8 @@ pub(crate) fn get_chat_commands() -> Vec<twilight_model::application::command::C
CommandBuilder::new("queue", "Print track queue", CommandType::ChatInput).build(), CommandBuilder::new("queue", "Print track queue", CommandType::ChatInput).build(),
CommandBuilder::new("resume", "Resume playing", CommandType::ChatInput).build(), CommandBuilder::new("resume", "Resume playing", CommandType::ChatInput).build(),
CommandBuilder::new("stop", "Stop playing", CommandType::ChatInput).build(), CommandBuilder::new("stop", "Stop playing", CommandType::ChatInput).build(),
CommandBuilder::new("delete", "Delete messages in chat", CommandType::ChatInput)
.option(IntegerBuilder::new("count", "How many messages to delete").required(true))
.build(),
] ]
} }

View File

@@ -1,28 +1,46 @@
use crate::state::State; use crate::state::State;
use std::error::Error; use std::error::Error;
use twilight_model::channel::Message; use twilight_model::{
application::interaction::Interaction,
channel::message::MessageFlags,
http::interaction::{InteractionResponse, InteractionResponseType},
};
use twilight_util::builder::InteractionResponseDataBuilder;
pub(crate) async fn pause( pub(crate) async fn pause(
msg: Message, interaction: Interaction,
state: State, state: State,
) -> Result<(), Box<dyn Error + Send + Sync + 'static>> { ) -> Result<(), Box<dyn Error + Send + Sync + 'static>> {
tracing::debug!( tracing::debug!(
"pause command in channel {} by {}", "pause command in guild {:?} in channel {:?} by {:?}",
msg.channel_id, interaction.guild_id,
msg.author.name interaction.channel,
interaction.author(),
); );
let guild_id = msg.guild_id.unwrap(); let Some(guild_id) = interaction.guild_id else {
return Ok(());
};
if let Some(call_lock) = state.songbird.get(guild_id) { if let Some(call_lock) = state.songbird.get(guild_id) {
let call = call_lock.lock().await; let call = call_lock.lock().await;
call.queue().pause()?; call.queue().pause()?;
} }
let interaction_response_data = InteractionResponseDataBuilder::new()
.content("Paused the track")
.flags(MessageFlags::EPHEMERAL)
.build();
let response = InteractionResponse {
kind: InteractionResponseType::ChannelMessageWithSource,
data: Some(interaction_response_data),
};
state state
.http .http
.create_message(msg.channel_id) .interaction(interaction.application_id)
.content("Paused the track")? .create_response(interaction.id, &interaction.token, &response)
.await?; .await?;
Ok(()) Ok(())

View File

@@ -1,4 +1,4 @@
use crate::commands::join; use crate::commands::join::join_channel;
use crate::metadata::{Metadata, MetadataMap}; use crate::metadata::{Metadata, MetadataMap};
use crate::state::State; use crate::state::State;
use serde_json::Value; use serde_json::Value;
@@ -7,23 +7,31 @@ use std::io::{BufRead, BufReader};
use std::{error::Error, ops::Sub, time::Duration}; use std::{error::Error, ops::Sub, time::Duration};
use tokio::process::Command; use tokio::process::Command;
use tracing::debug; use tracing::debug;
use twilight_model::channel::Message; use twilight_model::application::interaction::Interaction;
use twilight_model::channel::message::MessageFlags;
use twilight_model::http::interaction::{InteractionResponse, InteractionResponseType};
use twilight_util::builder::InteractionResponseDataBuilder;
use url::Url; use url::Url;
pub(crate) async fn play( pub(crate) async fn play(
msg: Message, interaction: Interaction,
state: State, state: State,
query: String, query: String,
) -> Result<(), Box<dyn Error + Send + Sync + 'static>> { ) -> Result<(), Box<dyn Error + Send + Sync + 'static>> {
tracing::debug!( debug!(
"play command in channel {} by {}", "play command in channel {:?} by {:?}",
msg.channel_id, interaction.channel,
msg.author.name interaction.author(),
); );
join(msg.clone(), state.clone()).await?; let Some(user_id) = interaction.author_id() else {
return Ok(());
};
let Some(guild_id) = interaction.guild_id else {
return Ok(());
};
let guild_id = msg.guild_id.unwrap(); join_channel(state.clone(), guild_id, user_id).await?;
// handle keyword queries // handle keyword queries
let query = if Url::parse(&query).is_err() { let query = if Url::parse(&query).is_err() {
@@ -32,6 +40,24 @@ pub(crate) async fn play(
query query
}; };
debug!("query: {:?}", query);
let interaction_response_data = InteractionResponseDataBuilder::new()
.content("Adding tracks to the queue ...")
.flags(MessageFlags::EPHEMERAL)
.build();
let response = InteractionResponse {
kind: InteractionResponseType::ChannelMessageWithSource,
data: Some(interaction_response_data),
};
state
.http
.interaction(interaction.application_id)
.create_response(interaction.id, &interaction.token, &response)
.await?;
// handle playlist links // handle playlist links
let urls = if query.contains("list=") { let urls = if query.contains("list=") {
get_playlist_urls(query).await? get_playlist_urls(query).await?
@@ -62,12 +88,6 @@ pub(crate) async fn play(
duration: metadata.duration, duration: metadata.duration,
}); });
} }
} else {
state
.http
.create_message(msg.channel_id)
.content("Cannot find any results")?
.await?;
} }
} }

View File

@@ -1,18 +1,26 @@
use twilight_model::http::interaction::InteractionResponseType;
use twilight_model::{
application::interaction::Interaction, channel::message::MessageFlags,
http::interaction::InteractionResponse,
};
use twilight_util::builder::InteractionResponseDataBuilder;
use crate::{metadata::MetadataMap, state::State}; use crate::{metadata::MetadataMap, state::State};
use std::error::Error; use std::error::Error;
use twilight_model::channel::Message;
pub(crate) async fn queue( pub(crate) async fn queue(
msg: Message, interaction: Interaction,
state: State, state: State,
) -> Result<(), Box<dyn Error + Send + Sync + 'static>> { ) -> Result<(), Box<dyn Error + Send + Sync + 'static>> {
tracing::debug!( tracing::debug!(
"queue command in channel {} by {}", "queue command in guild {:?} in channel {:?} by {:?}",
msg.channel_id, interaction.guild_id,
msg.author.name interaction.channel,
interaction.author(),
); );
let guild_id = msg.guild_id.unwrap(); let Some(guild_id) = interaction.guild_id else {
return Ok(());
};
if let Some(call_lock) = state.songbird.get(guild_id) { if let Some(call_lock) = state.songbird.get(guild_id) {
let call = call_lock.lock().await; let call = call_lock.lock().await;
let queue = call.queue().current_queue(); let queue = call.queue().current_queue();
@@ -48,10 +56,21 @@ pub(crate) async fn queue(
} }
message.push_str("`\n"); message.push_str("`\n");
} }
let interaction_response_data = InteractionResponseDataBuilder::new()
.content(&message)
.flags(MessageFlags::EPHEMERAL)
.build();
let response = InteractionResponse {
kind: InteractionResponseType::ChannelMessageWithSource,
data: Some(interaction_response_data),
};
state state
.http .http
.create_message(msg.channel_id) .interaction(interaction.application_id)
.content(&message)? .create_response(interaction.id, &interaction.token, &response)
.await?; .await?;
} }
Ok(()) Ok(())

View File

@@ -1,28 +1,45 @@
use crate::state::State; use crate::state::State;
use std::error::Error; use std::error::Error;
use twilight_model::channel::Message; use twilight_model::{
application::interaction::Interaction,
channel::message::MessageFlags,
http::interaction::{InteractionResponse, InteractionResponseType},
};
use twilight_util::builder::InteractionResponseDataBuilder;
pub(crate) async fn resume( pub(crate) async fn resume(
msg: Message, interaction: Interaction,
state: State, state: State,
) -> Result<(), Box<dyn Error + Send + Sync + 'static>> { ) -> Result<(), Box<dyn Error + Send + Sync + 'static>> {
tracing::debug!( tracing::debug!(
"resume command in channel {} by {}", "resume command in guild {:?} in channel {:?} by {:?}",
msg.channel_id, interaction.guild_id,
msg.author.name interaction.channel,
interaction.author(),
); );
let guild_id = msg.guild_id.unwrap(); let Some(guild_id) = interaction.guild_id else {
return Ok(());
};
if let Some(call_lock) = state.songbird.get(guild_id) { if let Some(call_lock) = state.songbird.get(guild_id) {
let call = call_lock.lock().await; let call = call_lock.lock().await;
call.queue().resume()?; call.queue().resume()?;
} }
let interaction_response_data = InteractionResponseDataBuilder::new()
.content("Resumed the track")
.flags(MessageFlags::EPHEMERAL)
.build();
let response = InteractionResponse {
kind: InteractionResponseType::ChannelMessageWithSource,
data: Some(interaction_response_data),
};
state state
.http .http
.create_message(msg.channel_id) .interaction(interaction.application_id)
.content("Resumed the track")? .create_response(interaction.id, &interaction.token, &response)
.await?; .await?;
Ok(()) Ok(())

View File

@@ -1,28 +1,47 @@
use twilight_model::{
application::interaction::Interaction,
channel::message::MessageFlags,
http::interaction::{InteractionResponse, InteractionResponseType},
};
use twilight_util::builder::InteractionResponseDataBuilder;
use crate::state::State; use crate::state::State;
use std::error::Error; use std::error::Error;
use twilight_model::channel::Message;
pub(crate) async fn stop( pub(crate) async fn stop(
msg: Message, interaction: Interaction,
state: State, state: State,
) -> Result<(), Box<dyn Error + Send + Sync + 'static>> { ) -> Result<(), Box<dyn Error + Send + Sync + 'static>> {
tracing::debug!( tracing::debug!(
"stop command in channel {} by {}", "stop command in guild {:?} in channel {:?} by {:?}",
msg.channel_id, interaction.guild_id,
msg.author.name interaction.channel,
interaction.author(),
); );
let guild_id = msg.guild_id.unwrap(); let Some(guild_id) = interaction.guild_id else {
return Ok(());
};
if let Some(call_lock) = state.songbird.get(guild_id) { if let Some(call_lock) = state.songbird.get(guild_id) {
let call = call_lock.lock().await; let call = call_lock.lock().await;
call.queue().stop(); call.queue().stop();
} }
let interaction_response_data = InteractionResponseDataBuilder::new()
.content("Stopped the track and cleared the queue")
.flags(MessageFlags::EPHEMERAL)
.build();
let response = InteractionResponse {
kind: InteractionResponseType::ChannelMessageWithSource,
data: Some(interaction_response_data),
};
state state
.http .http
.create_message(msg.channel_id) .interaction(interaction.application_id)
.content("Stopped the track and cleared the queue")? .create_response(interaction.id, &interaction.token, &response)
.await?; .await?;
Ok(()) Ok(())

View File

@@ -4,47 +4,25 @@ use crate::state::State;
use futures::Future; use futures::Future;
use std::error::Error; use std::error::Error;
use std::sync::Arc; use std::sync::Arc;
use tracing::debug;
use twilight_model::application::interaction::application_command::CommandOptionValue;
use twilight_model::application::interaction::{Interaction, InteractionData};
use twilight_gateway::Event; use twilight_gateway::Event;
use twilight_model::channel::Message;
enum ChatCommand { #[derive(Debug)]
Play(Message, String), enum InteractionCommand {
Stop(Message), Play(Interaction, String),
Pause(Message), Stop(Interaction),
Resume(Message), Pause(Interaction),
Leave(Message), Resume(Interaction),
Join(Message), Leave(Interaction),
Queue(Message), Join(Interaction),
Delete(Message), Queue(Interaction),
Delete(Interaction, i64),
NotImplemented, NotImplemented,
} }
fn parse_command(event: Event) -> Option<ChatCommand> {
match event {
Event::MessageCreate(msg_create) => {
if msg_create.guild_id.is_none() || !msg_create.content.starts_with('!') {
return None;
}
let split: Vec<&str> = msg_create.content.splitn(2, ' ').collect();
match split.as_slice() {
["!play", query] => {
Some(ChatCommand::Play(msg_create.0.clone(), query.to_string()))
}
["!stop"] | ["!stop", _] => Some(ChatCommand::Stop(msg_create.0)),
["!pause"] | ["!pause", _] => Some(ChatCommand::Pause(msg_create.0)),
["!resume"] | ["!resume", _] => Some(ChatCommand::Resume(msg_create.0)),
["!leave"] | ["!leave", _] => Some(ChatCommand::Leave(msg_create.0)),
["!join"] | ["!join", _] => Some(ChatCommand::Join(msg_create.0)),
["!queue"] | ["!queue", _] => Some(ChatCommand::Queue(msg_create.0)),
["!delete"] | ["!delete", _] => Some(ChatCommand::Delete(msg_create.0)),
_ => Some(ChatCommand::NotImplemented),
}
}
_ => None,
}
}
fn spawn( fn spawn(
fut: impl Future<Output = Result<(), Box<dyn Error + Send + Sync + 'static>>> + Send + 'static, fut: impl Future<Output = Result<(), Box<dyn Error + Send + Sync + 'static>>> + Send + 'static,
) { ) {
@@ -63,17 +41,84 @@ impl Handler {
pub(crate) fn new(state: State) -> Self { pub(crate) fn new(state: State) -> Self {
Self { state } Self { state }
} }
pub(crate) async fn act(&mut self, event: Event) { pub(crate) async fn act(&self, event: Event) {
match parse_command(event) { let interaction_command = match event {
Some(ChatCommand::Play(msg, query)) => spawn(play(msg, Arc::clone(&self.state), query)), Event::InteractionCreate(interaction) => {
Some(ChatCommand::Stop(msg)) => spawn(stop(msg, Arc::clone(&self.state))), debug!("interaction: {:?}", &interaction);
Some(ChatCommand::Pause(msg)) => spawn(pause(msg, Arc::clone(&self.state))), match &interaction.data {
Some(ChatCommand::Resume(msg)) => spawn(resume(msg, Arc::clone(&self.state))), Some(InteractionData::ApplicationCommand(command)) => {
Some(ChatCommand::Leave(msg)) => spawn(leave(msg, Arc::clone(&self.state))), debug!("command: {:?}", command);
Some(ChatCommand::Join(msg)) => spawn(join(msg, Arc::clone(&self.state))), match command.name.as_str() {
Some(ChatCommand::Queue(msg)) => spawn(queue(msg, Arc::clone(&self.state))), "play" => {
Some(ChatCommand::Delete(msg)) => spawn(delete(msg, Arc::clone(&self.state))), if let Some(query_option) =
command.options.iter().find(|opt| opt.name == "query")
{
if let CommandOptionValue::String(query) = &query_option.value {
InteractionCommand::Play(
interaction.0.clone(),
query.clone(),
)
} else {
InteractionCommand::NotImplemented
}
} else {
InteractionCommand::NotImplemented
}
}
"stop" => InteractionCommand::Stop(interaction.0.clone()),
"pause" => InteractionCommand::Pause(interaction.0.clone()),
"resume" => InteractionCommand::Resume(interaction.0.clone()),
"leave" => InteractionCommand::Leave(interaction.0.clone()),
"join" => InteractionCommand::Join(interaction.0.clone()),
"queue" => InteractionCommand::Queue(interaction.0.clone()),
"delete" => {
if let Some(count_option) =
command.options.iter().find(|opt| opt.name == "count")
{
if let CommandOptionValue::Integer(count) = count_option.value {
InteractionCommand::Delete(interaction.0.clone(), count)
} else {
InteractionCommand::NotImplemented
}
} else {
InteractionCommand::NotImplemented
}
}
_ => InteractionCommand::NotImplemented,
}
}
_ => InteractionCommand::NotImplemented,
}
}
_ => InteractionCommand::NotImplemented,
};
debug!("{:?}", interaction_command);
match interaction_command {
InteractionCommand::Play(interaction, query) => {
spawn(play(interaction, Arc::clone(&self.state), query))
}
InteractionCommand::Stop(interaction) => {
spawn(stop(interaction, Arc::clone(&self.state)))
}
InteractionCommand::Pause(interaction) => {
spawn(pause(interaction, Arc::clone(&self.state)))
}
InteractionCommand::Resume(interaction) => {
spawn(resume(interaction, Arc::clone(&self.state)))
}
InteractionCommand::Leave(interaction) => {
spawn(leave(interaction, Arc::clone(&self.state)))
}
InteractionCommand::Join(interaction) => {
spawn(join(interaction, Arc::clone(&self.state)))
}
InteractionCommand::Queue(interaction) => {
spawn(queue(interaction, Arc::clone(&self.state)))
}
InteractionCommand::Delete(interaction, count) => {
spawn(delete(interaction, Arc::clone(&self.state), count))
}
_ => {} _ => {}
} };
} }
} }

View File

@@ -82,7 +82,7 @@ async fn main() -> Result<(), Box<dyn Error + Send + Sync + 'static>> {
info!("Ready to receive events"); info!("Ready to receive events");
let mut handler = Handler::new(Arc::clone(&state)); let handler = Handler::new(Arc::clone(&state));
let mut stop_rx = signal_handler(); let mut stop_rx = signal_handler();
let mut stream = ShardEventStream::new(shards.iter_mut()); let mut stream = ShardEventStream::new(shards.iter_mut());
loop { loop {