Compare commits
2 Commits
288a784870
...
7d3a309d2b
| Author | SHA1 | Date | |
|---|---|---|---|
| 7d3a309d2b | |||
| d2511f7a55 |
34
Cargo.lock
generated
34
Cargo.lock
generated
@@ -578,6 +578,24 @@ dependencies = [
|
|||||||
"serde_core",
|
"serde_core",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "capnp"
|
||||||
|
version = "0.25.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3d1c82ec25a9501d60e22eef4be1b2c271769b5a96e224d0875baef28529cf30"
|
||||||
|
dependencies = [
|
||||||
|
"embedded-io",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "capnpc"
|
||||||
|
version = "0.25.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "fca02be865c8c5a78bfc24b9819006ab6b59bef238467203928e26459557af93"
|
||||||
|
dependencies = [
|
||||||
|
"capnp",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cargo-platform"
|
name = "cargo-platform"
|
||||||
version = "0.1.9"
|
version = "0.1.9"
|
||||||
@@ -1409,6 +1427,12 @@ dependencies = [
|
|||||||
"zeroize",
|
"zeroize",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "embedded-io"
|
||||||
|
version = "0.7.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9eb1aa714776b75c7e67e1da744b81a129b3ff919c8712b5e1b32252c1f07cc7"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "encoding_rs"
|
name = "encoding_rs"
|
||||||
version = "0.8.35"
|
version = "0.8.35"
|
||||||
@@ -1628,6 +1652,8 @@ checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
|
|||||||
name = "fomo-reducer"
|
name = "fomo-reducer"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"capnp",
|
||||||
|
"capnpc",
|
||||||
"clap",
|
"clap",
|
||||||
"dashmap 6.1.0",
|
"dashmap 6.1.0",
|
||||||
"futures",
|
"futures",
|
||||||
@@ -1639,6 +1665,7 @@ dependencies = [
|
|||||||
"snafu",
|
"snafu",
|
||||||
"songbird",
|
"songbird",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
"tokio-util",
|
||||||
"tracing",
|
"tracing",
|
||||||
"tracing-subscriber",
|
"tracing-subscriber",
|
||||||
"twilight-gateway",
|
"twilight-gateway",
|
||||||
@@ -6473,7 +6500,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "0136791f7c95b1f6dd99f9cc786b91bb81c3800b639b3478e561ddb7be95e5f1"
|
checksum = "0136791f7c95b1f6dd99f9cc786b91bb81c3800b639b3478e561ddb7be95e5f1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"fastrand",
|
"fastrand",
|
||||||
"getrandom 0.3.4",
|
"getrandom 0.4.2",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"rustix",
|
"rustix",
|
||||||
"windows-sys 0.61.2",
|
"windows-sys 0.61.2",
|
||||||
@@ -6714,9 +6741,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tokio-util"
|
name = "tokio-util"
|
||||||
version = "0.7.17"
|
version = "0.7.18"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2efa149fe76073d6e8fd97ef4f4eca7b67f599660115591483572e406e165594"
|
checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"futures-core",
|
"futures-core",
|
||||||
@@ -7042,6 +7069,7 @@ dependencies = [
|
|||||||
"tokio-websockets 0.13.2",
|
"tokio-websockets 0.13.2",
|
||||||
"tracing",
|
"tracing",
|
||||||
"twilight-gateway-queue",
|
"twilight-gateway-queue",
|
||||||
|
"twilight-http",
|
||||||
"twilight-model",
|
"twilight-model",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
11
Cargo.toml
11
Cargo.toml
@@ -4,6 +4,7 @@ version = "0.1.0"
|
|||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
capnp = "0.25.3"
|
||||||
clap = { version = "4.5.40", features = ["derive", "env"] }
|
clap = { version = "4.5.40", features = ["derive", "env"] }
|
||||||
dashmap = "6.1.0"
|
dashmap = "6.1.0"
|
||||||
futures = "0.3.32"
|
futures = "0.3.32"
|
||||||
@@ -57,12 +58,11 @@ songbird = { version = "0.6.0", default-features = false, features = [
|
|||||||
"twilight",
|
"twilight",
|
||||||
"tws",
|
"tws",
|
||||||
] }
|
] }
|
||||||
tokio = { version = "1.46.0", features = ["rt-multi-thread", "macros"] }
|
tokio = { version = "1.46.0", features = ["rt-multi-thread", "macros", "signal"] }
|
||||||
|
tokio-util = "0.7.18"
|
||||||
tracing = "0.1.41"
|
tracing = "0.1.41"
|
||||||
tracing-subscriber = { version = "0.3.19", features = ["env-filter"] }
|
tracing-subscriber = { version = "0.3.19", features = ["env-filter"] }
|
||||||
twilight-gateway = { version = "0.17", default-features = false, features = [
|
twilight-gateway = { version = "0.17", default-features = false, features = ["rustls-webpki-roots", "twilight-http"] }
|
||||||
"rustls-webpki-roots",
|
|
||||||
] }
|
|
||||||
twilight-http = { version = "0.17", default-features = false, features = [
|
twilight-http = { version = "0.17", default-features = false, features = [
|
||||||
"rustls-webpki-roots",
|
"rustls-webpki-roots",
|
||||||
"hickory",
|
"hickory",
|
||||||
@@ -71,3 +71,6 @@ twilight-http = { version = "0.17", default-features = false, features = [
|
|||||||
twilight-model = "0.17"
|
twilight-model = "0.17"
|
||||||
twilight-util = { version = "0.17", features = ["builder"] }
|
twilight-util = { version = "0.17", features = ["builder"] }
|
||||||
typed-builder = "0.23.2"
|
typed-builder = "0.23.2"
|
||||||
|
|
||||||
|
[build-dependencies]
|
||||||
|
capnpc = "0.25.3"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use std::sync::LazyLock;
|
use crate::{VCs, command::State};
|
||||||
|
|
||||||
use snafu::{OptionExt, Snafu};
|
use snafu::{OptionExt, Snafu};
|
||||||
|
use std::sync::LazyLock;
|
||||||
use twilight_model::{
|
use twilight_model::{
|
||||||
application::{
|
application::{
|
||||||
command::{Command, CommandType},
|
command::{Command, CommandType},
|
||||||
@@ -17,8 +17,6 @@ use twilight_util::builder::{
|
|||||||
InteractionResponseDataBuilder, command::CommandBuilder, embed::EmbedBuilder,
|
InteractionResponseDataBuilder, command::CommandBuilder, embed::EmbedBuilder,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{VCs, command::State};
|
|
||||||
|
|
||||||
const NAME: &str = "join";
|
const NAME: &str = "join";
|
||||||
const DESCRIPTION: &str = "The bot will join the same VC as you (with intention to record)";
|
const DESCRIPTION: &str = "The bot will join the same VC as you (with intention to record)";
|
||||||
|
|
||||||
@@ -30,7 +28,7 @@ pub static COMMAND: LazyLock<Command> = LazyLock::new(|| {
|
|||||||
});
|
});
|
||||||
|
|
||||||
#[derive(Debug, Snafu)]
|
#[derive(Debug, Snafu)]
|
||||||
enum GetGuildAndChannelIdError {
|
enum GetGuildAndVoiceChannelIdError {
|
||||||
/// this command was not used inside a guild (Discord server)
|
/// this command was not used inside a guild (Discord server)
|
||||||
NotInGuild,
|
NotInGuild,
|
||||||
|
|
||||||
@@ -44,29 +42,11 @@ enum GetGuildAndChannelIdError {
|
|||||||
UserNotInVC,
|
UserNotInVC,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<GetGuildAndChannelIdError> for Embed {
|
|
||||||
fn from(error: GetGuildAndChannelIdError) -> Embed {
|
|
||||||
match error {
|
|
||||||
GetGuildAndChannelIdError::NotInGuild => {
|
|
||||||
EmbedBuilder::new().title("Use this in a server").description("This bot can't find a VC to join if the command is used outside of a server (you might've used it in a DM?).").validate().unwrap().build()
|
|
||||||
}
|
|
||||||
GetGuildAndChannelIdError::NoUser => EmbedBuilder::new().title("Not invoked by a user").description("This command works by joining the same VC as the user, but this bot didn't receive any user data. So did no user invoke it?! (This error should be impossible!)").validate().unwrap().build(),
|
|
||||||
GetGuildAndChannelIdError::NoVCsInGuild => {
|
|
||||||
EmbedBuilder::new().title("No VCs in this server").description("This bot can't find a VC to join because there aren't any in this server right now.").validate().unwrap().build()
|
|
||||||
},
|
|
||||||
GetGuildAndChannelIdError::UserNotInVC => {
|
|
||||||
|
|
||||||
EmbedBuilder::new().title("You're not in a VC").description("This bot can't follow you into VC if you aren't in one in this server.").validate().unwrap().build()
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tracing::instrument]
|
#[tracing::instrument]
|
||||||
fn get_guild_and_channel_id(
|
fn get_guild_and_voice_channel_id(
|
||||||
interaction: &Interaction,
|
interaction: &Interaction,
|
||||||
vcs: &VCs,
|
vcs: &VCs,
|
||||||
) -> Result<(Id<GuildMarker>, Id<ChannelMarker>), GetGuildAndChannelIdError> {
|
) -> Result<(Id<GuildMarker>, Id<ChannelMarker>), GetGuildAndVoiceChannelIdError> {
|
||||||
let guild_id = interaction.guild_id.context(NotInGuildSnafu)?;
|
let guild_id = interaction.guild_id.context(NotInGuildSnafu)?;
|
||||||
|
|
||||||
let user_id = interaction
|
let user_id = interaction
|
||||||
@@ -77,17 +57,34 @@ fn get_guild_and_channel_id(
|
|||||||
|
|
||||||
let guild_vcs = vcs.get(&guild_id).context(NoVCsInGuildSnafu)?;
|
let guild_vcs = vcs.get(&guild_id).context(NoVCsInGuildSnafu)?;
|
||||||
|
|
||||||
let &channel_id = guild_vcs.get_left_for(&user_id).context(UserNotInVCSnafu)?;
|
let &voice_channel_id = guild_vcs.get_left_for(&user_id).context(UserNotInVCSnafu)?;
|
||||||
|
|
||||||
Ok((guild_id, channel_id))
|
Ok((guild_id, voice_channel_id))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_guild_and_vc_error_to_embed(error: GetGuildAndVoiceChannelIdError) -> Embed {
|
||||||
|
match error {
|
||||||
|
GetGuildAndVoiceChannelIdError::NotInGuild => {
|
||||||
|
EmbedBuilder::new().title("Use this in a server").description("This bot can't find a VC to join if the command is used outside of a server (you might've used it in a DM?).").validate().unwrap().build()
|
||||||
|
},
|
||||||
|
GetGuildAndVoiceChannelIdError::NoUser => {
|
||||||
|
EmbedBuilder::new().title("Not invoked by a user").description("This command works by joining the same VC as the user, but this bot didn't receive any user data. So did no user invoke it?! (This error should be impossible!)").validate().unwrap().build()
|
||||||
|
},
|
||||||
|
GetGuildAndVoiceChannelIdError::NoVCsInGuild => {
|
||||||
|
EmbedBuilder::new().title("No VCs in this server").description("This bot can't find a VC to join because there aren't any in this server right now.").validate().unwrap().build()
|
||||||
|
},
|
||||||
|
GetGuildAndVoiceChannelIdError::UserNotInVC => {
|
||||||
|
EmbedBuilder::new().title("You're not in a VC").description("This bot can't follow you into VC if you aren't in one in this server.").validate().unwrap().build()
|
||||||
|
},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tracing::instrument(skip(state))]
|
#[tracing::instrument(skip(state))]
|
||||||
pub async fn handle(state: State, interaction: Interaction) {
|
pub async fn handle(state: State, interaction: Interaction) {
|
||||||
let vcs = state.vcs;
|
let vcs = state.vcs;
|
||||||
|
|
||||||
let (guild_id, channel_id) = match get_guild_and_channel_id(&interaction, &vcs) {
|
let (guild_id, voice_channel_id) = match get_guild_and_voice_channel_id(&interaction, &vcs) {
|
||||||
Ok((guild_id, channel_id)) => (guild_id, channel_id),
|
Ok((guild_id, voice_channel_id)) => (guild_id, voice_channel_id),
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
state
|
state
|
||||||
.discord_client
|
.discord_client
|
||||||
@@ -99,7 +96,7 @@ pub async fn handle(state: State, interaction: Interaction) {
|
|||||||
kind: InteractionResponseType::ChannelMessageWithSource,
|
kind: InteractionResponseType::ChannelMessageWithSource,
|
||||||
data: Some(
|
data: Some(
|
||||||
InteractionResponseDataBuilder::new()
|
InteractionResponseDataBuilder::new()
|
||||||
.embeds([error.into()])
|
.embeds([get_guild_and_vc_error_to_embed(error)])
|
||||||
.flags(MessageFlags::EPHEMERAL)
|
.flags(MessageFlags::EPHEMERAL)
|
||||||
.build(),
|
.build(),
|
||||||
),
|
),
|
||||||
@@ -126,24 +123,15 @@ pub async fn handle(state: State, interaction: Interaction) {
|
|||||||
.await
|
.await
|
||||||
.expect("TODO");
|
.expect("TODO");
|
||||||
|
|
||||||
let call = loop {
|
let call = state
|
||||||
tracing::error!("TODO: about to try joining");
|
.songbird
|
||||||
|
.join(guild_id, voice_channel_id)
|
||||||
|
.await
|
||||||
|
.expect("TODO");
|
||||||
|
|
||||||
match state.songbird.join(guild_id, channel_id).await {
|
tracing::error!(?call, "successfully joined");
|
||||||
Ok(call) => break call,
|
|
||||||
Err(error) => {
|
|
||||||
tracing::error!(?error, "I'm still here");
|
|
||||||
|
|
||||||
if error.should_leave_server() {
|
let channel_mention = format!("<#{voice_channel_id}>");
|
||||||
state.songbird.leave(guild_id).await.expect("TODO");
|
|
||||||
} else if error.should_reconnect_driver() {
|
|
||||||
todo!();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let channel_mention = format!("<#{channel_id}>");
|
|
||||||
|
|
||||||
state
|
state
|
||||||
.discord_client
|
.discord_client
|
||||||
@@ -155,9 +143,4 @@ pub async fn handle(state: State, interaction: Interaction) {
|
|||||||
]))
|
]))
|
||||||
.await
|
.await
|
||||||
.expect("TODO");
|
.expect("TODO");
|
||||||
|
|
||||||
tracing::error!(?call, "TODO");
|
|
||||||
|
|
||||||
let call_guard = call.lock().await;
|
|
||||||
tracing::error!(?call_guard, "TODO");
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,23 @@
|
|||||||
use std::sync::LazyLock;
|
use crate::VCs;
|
||||||
|
|
||||||
use twilight_model::application::{
|
|
||||||
command::{Command, CommandType},
|
|
||||||
interaction::Interaction,
|
|
||||||
};
|
|
||||||
use twilight_util::builder::command::CommandBuilder;
|
|
||||||
|
|
||||||
use crate::command::State;
|
use crate::command::State;
|
||||||
|
use snafu::{OptionExt, Snafu};
|
||||||
|
use std::sync::LazyLock;
|
||||||
|
use twilight_model::channel::message::{Embed, MessageFlags};
|
||||||
|
use twilight_model::http::interaction::{InteractionResponse, InteractionResponseType};
|
||||||
|
use twilight_model::id::marker::UserMarker;
|
||||||
|
use twilight_model::{
|
||||||
|
application::{
|
||||||
|
command::{Command, CommandType},
|
||||||
|
interaction::Interaction,
|
||||||
|
},
|
||||||
|
id::{
|
||||||
|
Id,
|
||||||
|
marker::{ChannelMarker, GuildMarker},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
use twilight_util::builder::InteractionResponseDataBuilder;
|
||||||
|
use twilight_util::builder::command::CommandBuilder;
|
||||||
|
use twilight_util::builder::embed::EmbedBuilder;
|
||||||
|
|
||||||
const NAME: &str = "leave";
|
const NAME: &str = "leave";
|
||||||
const DESCRIPTION: &str = "The bot will leave the VC it's in (so it won't record anyone anymore)";
|
const DESCRIPTION: &str = "The bot will leave the VC it's in (so it won't record anyone anymore)";
|
||||||
@@ -18,7 +29,136 @@ pub static COMMAND: LazyLock<Command> = LazyLock::new(|| {
|
|||||||
.build()
|
.build()
|
||||||
});
|
});
|
||||||
|
|
||||||
|
#[derive(Debug, Snafu)]
|
||||||
|
pub enum GetGuildAndVoiceChannelIdError {
|
||||||
|
/// this command was not used inside a guild (Discord server)
|
||||||
|
NotInGuild,
|
||||||
|
|
||||||
|
/// there is no user who invoked this command
|
||||||
|
NoUser,
|
||||||
|
|
||||||
|
/// there are no voice chats in this guild
|
||||||
|
NoVCsInGuild,
|
||||||
|
|
||||||
|
/// the bot is not in a voice chat in this guild
|
||||||
|
BotNotInVC,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument]
|
||||||
|
pub fn get_user_and_guild_and_voice_channel_id(
|
||||||
|
bot_user_id: Id<UserMarker>,
|
||||||
|
interaction: &Interaction,
|
||||||
|
vcs: &VCs,
|
||||||
|
) -> Result<(Id<UserMarker>, Id<GuildMarker>, Id<ChannelMarker>), GetGuildAndVoiceChannelIdError> {
|
||||||
|
let user_id = interaction
|
||||||
|
.member
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|member| member.user.as_ref().map(|user| user.id))
|
||||||
|
.context(NoUserSnafu)?;
|
||||||
|
|
||||||
|
let guild_id = interaction.guild_id.context(NotInGuildSnafu)?;
|
||||||
|
|
||||||
|
let guild_vcs = vcs.get(&guild_id).context(NoVCsInGuildSnafu)?;
|
||||||
|
|
||||||
|
let &voice_channel_id = guild_vcs
|
||||||
|
.get_left_for(&bot_user_id)
|
||||||
|
.context(BotNotInVCSnafu)?;
|
||||||
|
|
||||||
|
Ok((user_id, guild_id, voice_channel_id))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_guild_and_vc_error_to_embed(error: GetGuildAndVoiceChannelIdError) -> Embed {
|
||||||
|
match error {
|
||||||
|
GetGuildAndVoiceChannelIdError::NotInGuild => {
|
||||||
|
EmbedBuilder::new().title("Use this in a server").description("This bot can't tell which VC to leave if the command is used outside of a server (you might've used it in a DM?).").validate().unwrap().build()
|
||||||
|
},
|
||||||
|
GetGuildAndVoiceChannelIdError::NoUser => {
|
||||||
|
EmbedBuilder::new().title("Not invoked by a user").description("This command works by joining the same VC as the user, but this bot didn't receive any user data. So did no user invoke it?! (This error should be impossible!)").validate().unwrap().build()
|
||||||
|
},
|
||||||
|
GetGuildAndVoiceChannelIdError::NoVCsInGuild => {
|
||||||
|
EmbedBuilder::new().title("No VCs in this server").description("This bot can't leave VC because there aren't any in this server right now (therefore the bot must not be in any).").validate().unwrap().build()
|
||||||
|
},
|
||||||
|
GetGuildAndVoiceChannelIdError::BotNotInVC => {
|
||||||
|
EmbedBuilder::new().title("Not in a VC").description("This bot can't leave VC if it isn't in one in this server.").validate().unwrap().build()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[tracing::instrument]
|
#[tracing::instrument]
|
||||||
pub async fn handle(state: State, interaction: Interaction) {
|
pub async fn handle(state: State, interaction: Interaction) {
|
||||||
todo!();
|
let (user_id, guild_id, voice_channel_id) = match get_user_and_guild_and_voice_channel_id(
|
||||||
|
state.discord_user_id,
|
||||||
|
&interaction,
|
||||||
|
&state.vcs,
|
||||||
|
) {
|
||||||
|
Ok((user_id, guild_id, voice_channel_id)) => (user_id, guild_id, voice_channel_id),
|
||||||
|
Err(error) => {
|
||||||
|
state
|
||||||
|
.discord_client
|
||||||
|
.interaction(state.discord_application_id)
|
||||||
|
.create_response(
|
||||||
|
interaction.id,
|
||||||
|
&interaction.token,
|
||||||
|
&InteractionResponse {
|
||||||
|
kind: InteractionResponseType::ChannelMessageWithSource,
|
||||||
|
data: Some(
|
||||||
|
InteractionResponseDataBuilder::new()
|
||||||
|
.embeds([get_guild_and_vc_error_to_embed(error)])
|
||||||
|
.flags(MessageFlags::EPHEMERAL)
|
||||||
|
.build(),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.expect("TODO");
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if user_id != state.discord_bot_owner_user_id {
|
||||||
|
state
|
||||||
|
.discord_client
|
||||||
|
.interaction(state.discord_application_id)
|
||||||
|
.create_response(
|
||||||
|
interaction.id,
|
||||||
|
&interaction.token,
|
||||||
|
&InteractionResponse {
|
||||||
|
kind: InteractionResponseType::ChannelMessageWithSource,
|
||||||
|
data: Some(
|
||||||
|
InteractionResponseDataBuilder::new()
|
||||||
|
.embeds([
|
||||||
|
EmbedBuilder::new().title("No permission to make this bot leave").description("Only the owner of the bot is allowed to make the bot leave RIGHT NOW. You might be looking for the opt out command.").validate().unwrap().build()
|
||||||
|
])
|
||||||
|
.flags(MessageFlags::EPHEMERAL)
|
||||||
|
.build(),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.expect("TODO");
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
state.songbird.leave(guild_id).await.expect("TODO");
|
||||||
|
|
||||||
|
tracing::error!("TODO: successfully left the call");
|
||||||
|
|
||||||
|
let channel_mention = format!("<#{voice_channel_id}>");
|
||||||
|
|
||||||
|
state
|
||||||
|
.discord_client
|
||||||
|
.interaction(state.discord_application_id)
|
||||||
|
.update_response(&interaction.token)
|
||||||
|
.embeds(Some(&[EmbedBuilder::new()
|
||||||
|
.title("Left VC")
|
||||||
|
.description(format!(
|
||||||
|
"This bot left {channel_mention} (and is thereby unable to record anymore)."
|
||||||
|
))
|
||||||
|
.validate()
|
||||||
|
.unwrap()
|
||||||
|
.build()]))
|
||||||
|
.await
|
||||||
|
.expect("TODO");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,9 +3,13 @@ use std::{fmt::Debug, sync::Arc};
|
|||||||
use futures::future::BoxFuture;
|
use futures::future::BoxFuture;
|
||||||
use patricia_tree::StringPatriciaMap;
|
use patricia_tree::StringPatriciaMap;
|
||||||
use songbird::Songbird;
|
use songbird::Songbird;
|
||||||
|
use tokio_util::sync::CancellationToken;
|
||||||
use twilight_model::{
|
use twilight_model::{
|
||||||
application::{command::Command, interaction::Interaction},
|
application::{command::Command, interaction::Interaction},
|
||||||
id::{Id, marker::ApplicationMarker},
|
id::{
|
||||||
|
Id,
|
||||||
|
marker::{ApplicationMarker, UserMarker},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::VCs;
|
use crate::VCs;
|
||||||
@@ -16,8 +20,11 @@ mod opt_out;
|
|||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct State {
|
pub struct State {
|
||||||
pub discord_client: Arc<twilight_http::Client>,
|
pub cancellation_token: CancellationToken,
|
||||||
pub discord_application_id: Id<ApplicationMarker>,
|
pub discord_application_id: Id<ApplicationMarker>,
|
||||||
|
pub discord_bot_owner_user_id: Id<UserMarker>,
|
||||||
|
pub discord_client: Arc<twilight_http::Client>,
|
||||||
|
pub discord_user_id: Id<UserMarker>,
|
||||||
pub songbird: Arc<Songbird>,
|
pub songbird: Arc<Songbird>,
|
||||||
pub vcs: Arc<VCs>,
|
pub vcs: Arc<VCs>,
|
||||||
}
|
}
|
||||||
|
|||||||
115
src/main.rs
115
src/main.rs
@@ -5,6 +5,8 @@ use secrecy::{ExposeSecret, SecretString};
|
|||||||
use snafu::Snafu;
|
use snafu::Snafu;
|
||||||
use songbird::{Songbird, shards::TwilightMap};
|
use songbird::{Songbird, shards::TwilightMap};
|
||||||
use std::{fmt::Debug, str::FromStr, sync::Arc};
|
use std::{fmt::Debug, str::FromStr, sync::Arc};
|
||||||
|
use tokio::{select, signal::ctrl_c, task::JoinSet};
|
||||||
|
use tokio_util::{sync::CancellationToken, time::FutureExt as _};
|
||||||
use tracing_subscriber::{EnvFilter, fmt::format::FmtSpan};
|
use tracing_subscriber::{EnvFilter, fmt::format::FmtSpan};
|
||||||
use twilight_gateway::{Event, EventTypeFlags, Intents, Shard, ShardId, StreamExt};
|
use twilight_gateway::{Event, EventTypeFlags, Intents, Shard, ShardId, StreamExt};
|
||||||
use twilight_model::{
|
use twilight_model::{
|
||||||
@@ -14,12 +16,12 @@ use twilight_model::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
struct OpendalOperator {
|
struct Storage {
|
||||||
uri: OperatorUri,
|
uri: OperatorUri,
|
||||||
operator: Operator,
|
operator: Operator,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FromStr for OpendalOperator {
|
impl FromStr for Storage {
|
||||||
type Err = opendal::Error;
|
type Err = opendal::Error;
|
||||||
|
|
||||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
@@ -30,19 +32,19 @@ impl FromStr for OpendalOperator {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl OpendalOperator {
|
impl Storage {
|
||||||
fn into_inner(self) -> Operator {
|
fn into_inner(self) -> Operator {
|
||||||
self.operator
|
self.operator
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<OpendalOperator> for Operator {
|
impl From<Storage> for Operator {
|
||||||
fn from(wrapper: OpendalOperator) -> Self {
|
fn from(wrapper: Storage) -> Self {
|
||||||
wrapper.into_inner()
|
wrapper.into_inner()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Debug for OpendalOperator {
|
impl Debug for Storage {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
Debug::fmt(&self.uri, f)
|
Debug::fmt(&self.uri, f)
|
||||||
}
|
}
|
||||||
@@ -54,10 +56,16 @@ struct AppArgs {
|
|||||||
discord_token: SecretString,
|
discord_token: SecretString,
|
||||||
|
|
||||||
#[arg(long, env)]
|
#[arg(long, env)]
|
||||||
storage: OpendalOperator,
|
bot_owner: Id<UserMarker>,
|
||||||
|
|
||||||
#[arg(long, env)]
|
#[arg(long, env)]
|
||||||
bot_owner: Id<UserMarker>,
|
bot_data: Storage,
|
||||||
|
|
||||||
|
#[arg(long, env)]
|
||||||
|
user_data: Storage,
|
||||||
|
|
||||||
|
#[arg(long, env)]
|
||||||
|
recording_data: Storage,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Parser)]
|
#[derive(Parser)]
|
||||||
@@ -80,7 +88,10 @@ struct Args {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Snafu)]
|
#[derive(Debug, Snafu)]
|
||||||
enum MainError {}
|
enum MainError {
|
||||||
|
/// the program was cancelled, perhaps by Ctrl-C / SIGINT
|
||||||
|
Cancelled,
|
||||||
|
}
|
||||||
|
|
||||||
#[snafu::report]
|
#[snafu::report]
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
@@ -102,17 +113,21 @@ async fn main() -> Result<(), MainError> {
|
|||||||
|
|
||||||
let AppArgs {
|
let AppArgs {
|
||||||
discord_token,
|
discord_token,
|
||||||
storage,
|
|
||||||
bot_owner,
|
bot_owner,
|
||||||
|
bot_data,
|
||||||
|
user_data,
|
||||||
|
recording_data,
|
||||||
} = app_args;
|
} = app_args;
|
||||||
|
|
||||||
|
let cancellation_token = CancellationToken::new();
|
||||||
|
|
||||||
rustls::crypto::aws_lc_rs::default_provider()
|
rustls::crypto::aws_lc_rs::default_provider()
|
||||||
.install_default()
|
.install_default()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let discord_client = twilight_http::Client::new(discord_token.expose_secret().to_owned());
|
let discord_client = twilight_http::Client::new(discord_token.expose_secret().to_owned());
|
||||||
|
|
||||||
let user = discord_client
|
let discord_user = discord_client
|
||||||
.current_user()
|
.current_user()
|
||||||
.await
|
.await
|
||||||
.expect("couldn't fetch current user") // TODO
|
.expect("couldn't fetch current user") // TODO
|
||||||
@@ -120,7 +135,7 @@ async fn main() -> Result<(), MainError> {
|
|||||||
.await
|
.await
|
||||||
.expect("couldn't deserialize current user"); // TODO
|
.expect("couldn't deserialize current user"); // TODO
|
||||||
|
|
||||||
let user_id = user.id;
|
let discord_user_id = discord_user.id;
|
||||||
|
|
||||||
let current_application = discord_client
|
let current_application = discord_client
|
||||||
.current_user_application()
|
.current_user_application()
|
||||||
@@ -134,21 +149,26 @@ async fn main() -> Result<(), MainError> {
|
|||||||
|
|
||||||
let discord_application_id = current_application.id;
|
let discord_application_id = current_application.id;
|
||||||
|
|
||||||
let shard_id = ShardId::new(0, 1);
|
|
||||||
let intents = Intents::GUILD_VOICE_STATES;
|
let intents = Intents::GUILD_VOICE_STATES;
|
||||||
let mut shard = Shard::new(shard_id, discord_token.expose_secret().to_owned(), intents);
|
let config = twilight_gateway::Config::new(discord_token.expose_secret().to_owned(), intents);
|
||||||
|
|
||||||
let senders = TwilightMap::new(FromIterator::from_iter([(
|
let shards = twilight_gateway::create_recommended(&discord_client, config, |_id, builder| {
|
||||||
shard.id().number(),
|
builder.build()
|
||||||
shard.sender(),
|
})
|
||||||
)]));
|
.await
|
||||||
|
.expect("TODO");
|
||||||
|
let shards = Vec::from_iter(shards);
|
||||||
|
|
||||||
|
let senders = TwilightMap::new(
|
||||||
|
shards
|
||||||
|
.iter()
|
||||||
|
.map(|shard| (shard.id().number(), shard.sender()))
|
||||||
|
.collect(),
|
||||||
|
);
|
||||||
|
|
||||||
let senders = Arc::new(senders);
|
let senders = Arc::new(senders);
|
||||||
|
|
||||||
let songbird = Songbird::twilight(senders, user_id);
|
let songbird = Songbird::twilight(senders, discord_user_id);
|
||||||
|
|
||||||
let event_types = EventTypeFlags::GUILD_VOICE_STATES | EventTypeFlags::INTERACTION_CREATE;
|
|
||||||
let mut next_event = shard.next_event(event_types);
|
|
||||||
|
|
||||||
let interaction_client = discord_client.interaction(discord_application_id);
|
let interaction_client = discord_client.interaction(discord_application_id);
|
||||||
|
|
||||||
@@ -179,13 +199,55 @@ async fn main() -> Result<(), MainError> {
|
|||||||
let vcs = Arc::new(vcs);
|
let vcs = Arc::new(vcs);
|
||||||
|
|
||||||
let state = State {
|
let state = State {
|
||||||
|
cancellation_token: cancellation_token.clone(),
|
||||||
discord_application_id,
|
discord_application_id,
|
||||||
|
discord_bot_owner_user_id: bot_owner,
|
||||||
discord_client,
|
discord_client,
|
||||||
|
discord_user_id,
|
||||||
songbird,
|
songbird,
|
||||||
vcs,
|
vcs,
|
||||||
};
|
};
|
||||||
|
|
||||||
while let Some(event_res) = next_event.await {
|
let run_shards = JoinSet::from_iter(
|
||||||
|
shards
|
||||||
|
.into_iter()
|
||||||
|
.map(|shard| handle_events(command_router.clone(), state.clone(), shard)),
|
||||||
|
);
|
||||||
|
let run_shards = run_shards.join_all();
|
||||||
|
tokio::pin!(run_shards);
|
||||||
|
|
||||||
|
tokio::spawn({
|
||||||
|
let cancellation_token = cancellation_token.clone();
|
||||||
|
async move {
|
||||||
|
match ctrl_c().await {
|
||||||
|
Ok(()) => cancellation_token.cancel(),
|
||||||
|
Err(error) => tracing::error!(?error, "failed to listen for interrupt signal"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
select! {
|
||||||
|
_ = &mut run_shards => {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
() = cancellation_token.cancelled() => {
|
||||||
|
tracing::warn!("waiting for tasks to gracefully shut down");
|
||||||
|
run_shards.await;
|
||||||
|
|
||||||
|
Err(MainError::Cancelled)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument(skip(command_router, state))]
|
||||||
|
async fn handle_events(command_router: Arc<CommandRouter>, state: State, mut shard: Shard) {
|
||||||
|
let event_types = EventTypeFlags::GUILD_VOICE_STATES | EventTypeFlags::INTERACTION_CREATE;
|
||||||
|
|
||||||
|
while let Some(Some(event_res)) = shard
|
||||||
|
.next_event(event_types)
|
||||||
|
.with_cancellation_token(&state.cancellation_token)
|
||||||
|
.await
|
||||||
|
{
|
||||||
match event_res {
|
match event_res {
|
||||||
Ok(event) => {
|
Ok(event) => {
|
||||||
handle_event(command_router.clone(), state.clone(), event).await;
|
handle_event(command_router.clone(), state.clone(), event).await;
|
||||||
@@ -194,16 +256,13 @@ async fn main() -> Result<(), MainError> {
|
|||||||
tracing::error!(?error);
|
tracing::error!(?error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
next_event = shard.next_event(event_types);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tracing::instrument(skip(command_router))]
|
#[tracing::instrument(skip(command_router, state))]
|
||||||
async fn handle_event(command_router: Arc<CommandRouter>, state: State, event: Event) {
|
async fn handle_event(command_router: Arc<CommandRouter>, state: State, event: Event) {
|
||||||
state.songbird.process(&event).await;
|
state.songbird.process(&event).await;
|
||||||
|
|
||||||
match event {
|
match event {
|
||||||
Event::VoiceStateUpdate(voice_state_update) => {
|
Event::VoiceStateUpdate(voice_state_update) => {
|
||||||
update_vcs(&voice_state_update, &state.vcs);
|
update_vcs(&voice_state_update, &state.vcs);
|
||||||
|
|||||||
Reference in New Issue
Block a user