Functional bot: responds to commands, moves users

This commit is contained in:
bluepython508
2024-11-03 21:00:19 +00:00
parent 0b0950959b
commit 23169ef19c
4 changed files with 343 additions and 12 deletions

5
.envrc
View File

@@ -1 +1,6 @@
use flake
export DISCORD_TOKEN_FILE=$(mktemp)
cat >$DISCORD_TOKEN_FILE <<END
MTMwMjQyMTcwMjU5NjIzMTE5OA.G3qd1e.5LqVmTtO0ZxR-j1ueO537IvKHF8YfHdnd-R4zo
END

156
Cargo.lock generated
View File

@@ -4,13 +4,19 @@ version = 3
[[package]]
name = "addr2line"
version = "0.24.2"
version = "0.21.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1"
checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb"
dependencies = [
"gimli",
]
[[package]]
name = "adler"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
[[package]]
name = "adler2"
version = "2.0.0"
@@ -69,17 +75,17 @@ checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
[[package]]
name = "backtrace"
version = "0.3.74"
version = "0.3.71"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a"
checksum = "26b05800d2e817c8b3b4b54abd461726265fa9789ae34330622f2db9ee696f9d"
dependencies = [
"addr2line",
"cc",
"cfg-if",
"libc",
"miniz_oxide",
"miniz_oxide 0.7.4",
"object",
"rustc-demangle",
"windows-targets 0.52.6",
]
[[package]]
@@ -119,8 +125,13 @@ dependencies = [
name = "botc-discord"
version = "0.1.0"
dependencies = [
"color-eyre",
"eyre",
"futures",
"poise",
"rand",
"serenity",
"tokio",
]
[[package]]
@@ -206,6 +217,33 @@ dependencies = [
"windows-targets 0.52.6",
]
[[package]]
name = "color-eyre"
version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55146f5e46f237f7423d74111267d4597b59b0dad0ffaf7303bce9945d843ad5"
dependencies = [
"backtrace",
"color-spantrace",
"eyre",
"indenter",
"once_cell",
"owo-colors",
"tracing-error",
]
[[package]]
name = "color-spantrace"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cd6be1b2a7e382e2b98b43b2adcca6bb0e465af0bdd38123873ae61eb17a72c2"
dependencies = [
"once_cell",
"owo-colors",
"tracing-core",
"tracing-error",
]
[[package]]
name = "command_attr"
version = "0.5.2"
@@ -396,6 +434,16 @@ dependencies = [
"version_check",
]
[[package]]
name = "eyre"
version = "0.6.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7cd915d99f24784cdc19fd37ef22b97e3ff0ae756c7e492e9fbfe897d61e2aec"
dependencies = [
"indenter",
"once_cell",
]
[[package]]
name = "fastrand"
version = "2.1.1"
@@ -409,7 +457,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1b589b4dc103969ad3cf85c950899926ec64300a1a46d76c03a6072957036f0"
dependencies = [
"crc32fast",
"miniz_oxide",
"miniz_oxide 0.8.0",
]
[[package]]
@@ -435,6 +483,7 @@ checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876"
dependencies = [
"futures-channel",
"futures-core",
"futures-executor",
"futures-io",
"futures-sink",
"futures-task",
@@ -457,6 +506,17 @@ version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e"
[[package]]
name = "futures-executor"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f"
dependencies = [
"futures-core",
"futures-task",
"futures-util",
]
[[package]]
name = "futures-io"
version = "0.3.31"
@@ -536,9 +596,9 @@ dependencies = [
[[package]]
name = "gimli"
version = "0.31.1"
version = "0.28.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f"
checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253"
[[package]]
name = "glob"
@@ -705,6 +765,12 @@ dependencies = [
"unicode-normalization",
]
[[package]]
name = "indenter"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683"
[[package]]
name = "indexmap"
version = "2.6.0"
@@ -736,6 +802,12 @@ dependencies = [
"wasm-bindgen",
]
[[package]]
name = "lazy_static"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
[[package]]
name = "levenshtein"
version = "1.0.5"
@@ -807,6 +879,15 @@ dependencies = [
"triomphe",
]
[[package]]
name = "miniz_oxide"
version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08"
dependencies = [
"adler",
]
[[package]]
name = "miniz_oxide"
version = "0.8.0"
@@ -845,9 +926,9 @@ dependencies = [
[[package]]
name = "object"
version = "0.36.5"
version = "0.32.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aedf0a2d09c573ed1d8d85b30c119153926a2b36dce0ab28322c09a117a4683e"
checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441"
dependencies = [
"memchr",
]
@@ -858,6 +939,12 @@ version = "1.20.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775"
[[package]]
name = "owo-colors"
version = "3.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c1b04fb49957986fdce4d6ee7a65027d55d4b6d2265e5848bbb507b58ccfdb6f"
[[package]]
name = "parking_lot"
version = "0.12.3"
@@ -1331,6 +1418,15 @@ dependencies = [
"digest",
]
[[package]]
name = "sharded-slab"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6"
dependencies = [
"lazy_static",
]
[[package]]
name = "shlex"
version = "1.3.0"
@@ -1489,6 +1585,16 @@ dependencies = [
"syn 2.0.87",
]
[[package]]
name = "thread_local"
version = "1.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c"
dependencies = [
"cfg-if",
"once_cell",
]
[[package]]
name = "time"
version = "0.3.36"
@@ -1648,6 +1754,28 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54"
dependencies = [
"once_cell",
"valuable",
]
[[package]]
name = "tracing-error"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d686ec1c0f384b1277f097b2f279a2ecc11afe8c133c1aabf036a27cb4cd206e"
dependencies = [
"tracing",
"tracing-subscriber",
]
[[package]]
name = "tracing-subscriber"
version = "0.3.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b"
dependencies = [
"sharded-slab",
"thread_local",
"tracing-core",
]
[[package]]
@@ -1781,6 +1909,12 @@ version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f4bf03e0ca70d626ecc4ba6b0763b934b6f2976e8c744088bb3c1d646fbb1ad0"
[[package]]
name = "valuable"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d"
[[package]]
name = "version_check"
version = "0.9.5"

View File

@@ -6,5 +6,10 @@ edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
color-eyre = "0.6.3"
eyre = "0.6.12"
futures = "0.3.31"
poise = "0.6.1"
rand = "0.8.5"
serenity = "0.12.2"
tokio = { version = "1.41.0", features = ["rt", "net"] }

View File

@@ -1 +1,188 @@
fn main() {}
use std::{
collections::BTreeMap,
sync::{Arc, RwLock},
};
use eyre::{bail, Context as _, Error, OptionExt, Result};
use futures::FutureExt;
use poise::serenity_prelude as serenity;
use rand::seq::SliceRandom;
// TODO: persist configuration
// TODO: buttons?
#[derive(Copy, Clone)]
struct GuildData {
town_square: serenity::ChannelId,
cottages: serenity::ChannelId,
}
#[derive(Default)]
struct Data(Arc<RwLock<BTreeMap<serenity::GuildId, GuildData>>>);
type Context<'a> = poise::Context<'a, Data, Error>;
async fn run(ctx: Context<'_>, from: serenity::ChannelId, to: serenity::ChannelId) -> Result<()> {
let guild = ctx
.guild_id()
.ok_or_eyre("This bot only works in servers")?;
let channels = guild.channels(&ctx).await?;
let channels_for = |id| {
let channel = channels
.get(&id)
.ok_or_eyre(format!("The channel {id:?} doesn't exist"))?;
Ok::<_, Error>(match channel.kind {
serenity::ChannelType::Voice => vec![channel],
serenity::ChannelType::Category => channels
.values()
.filter(|chan| {
chan.kind == serenity::ChannelType::Voice && chan.parent_id == Some(channel.id)
})
.collect(),
ty => bail!(
"This bot doesn't work with non-voice channels: {:?} has type {:?}",
&channel.name,
ty
),
})
};
let mut users = channels_for(from)?
.into_iter()
.flat_map(|chan| chan.members(ctx).unwrap())
.collect::<Vec<_>>();
users.shuffle(&mut rand::thread_rng());
let to = channels_for(to)?;
futures::future::try_join_all(
to.into_iter()
.cycle()
.zip(&users)
.map(|(channel, user)| guild.move_member(ctx, user.user.id, channel)),
)
.await?;
ctx.reply(format!("Moved {} users", users.len())).await?;
Ok(())
}
#[poise::command(slash_command, prefix_command, ephemeral)]
async fn dusk(ctx: Context<'_>) -> Result<()> {
let guild = ctx
.guild_id()
.ok_or_eyre("This bot only works in servers")?;
let GuildData {
town_square,
cottages,
} = ctx
.data()
.0
.read()
.expect("No poisoned locks")
.get(&guild)
.copied()
.ok_or_eyre("This bot hasn't been configured for this server")?;
run(ctx, town_square, cottages).await?;
Ok(())
}
#[poise::command(slash_command, prefix_command, ephemeral)]
async fn dawn(ctx: Context<'_>) -> Result<()> {
let guild = ctx
.guild_id()
.ok_or_eyre("This bot only works in servers")?;
let GuildData {
town_square,
cottages,
} = ctx
.data()
.0
.read()
.expect("No poisoned locks")
.get(&guild)
.copied()
.ok_or_eyre("This bot hasn't been configured for this server")?;
run(ctx, cottages, town_square).await?;
Ok(())
}
#[poise::command(slash_command, prefix_command, ephemeral)]
async fn configure(
ctx: Context<'_>,
#[channel_types("Voice")] town_square: serenity::ChannelId,
#[channel_types("Category")] cottages: serenity::ChannelId,
) -> Result<()> {
let guild = ctx
.guild_id()
.ok_or_eyre("This bot only works in servers")?;
ctx.data().0.write().expect("No poisoned locks").insert(
guild,
GuildData {
town_square,
cottages,
},
);
ctx.reply("Configured successfully!").await?;
Ok(())
}
async fn register_commands(
ctx: impl AsRef<serenity::Http>,
options: &poise::FrameworkOptions<Data, Error>,
guild: serenity::GuildId,
) -> Result<()> {
poise::builtins::register_in_guild(ctx, &options.commands, guild).await?;
Ok(())
}
async fn on_event(
ctx: &serenity::Context,
event: &serenity::FullEvent,
framework: poise::FrameworkContext<'_, Data, Error>,
_: &Data,
) -> Result<()> {
if let serenity::FullEvent::GuildCreate { guild, is_new: _ } = event {
register_commands(ctx, framework.options, guild.id).await?;
};
Ok(())
}
#[tokio::main(flavor = "current_thread")]
async fn main() -> Result<()> {
color_eyre::install()?;
let token = std::fs::read_to_string(
std::env::var_os("DISCORD_TOKEN_FILE").ok_or_eyre("A Discord token is required")?,
)
.wrap_err("A Discord token is required")?;
let intents = serenity::GatewayIntents::non_privileged();
let framework = poise::Framework::builder()
.options(poise::FrameworkOptions {
commands: vec![dusk(), dawn(), configure()],
event_handler: |ctx, ev, ctxf, data| on_event(ctx, ev, ctxf, data).boxed(),
..Default::default()
})
.setup(|ctx, ready, framework| {
Box::pin(async move {
for guild in &ready.guilds {
register_commands(ctx, framework.options(), guild.id).await?;
}
Ok(Data::default())
})
})
.build();
serenity::ClientBuilder::new(token, intents)
.framework(framework)
.await?
.start()
.await?;
Ok(())
}