Compare commits

...

6 Commits

Author SHA1 Message Date
bluepython508
2565a53496 spectate command; remove spectate from ST; currently_playing improvements 2025-07-20 14:28:45 +01:00
bluepython508
1482e1770d Remove unnecessary logging of on_event: flooding the logs 2025-07-20 14:28:31 +01:00
bluepython508
e38ce4e568 Add logging 2025-05-03 22:12:10 +01:00
bluepython508
0e4df70f39 Buffer user moves - limit to 10 at a time
Hopefully mitigates the issues with hangs on larger calls
2025-01-30 15:08:13 +00:00
bluepython508
994babaacf Expand gitignore to include other dbs or tokens 2024-12-15 19:26:43 +00:00
bluepython508
06f116976d Switch to ephemeral defer for collect/spread to make responses properly ephemeral 2024-12-15 19:25:59 +00:00
4 changed files with 232 additions and 48 deletions

3
.gitignore vendored
View File

@@ -1,4 +1,5 @@
/target /target
/.direnv/ /.direnv/
/db.sqlite /*.sqlite
/token /token
/token-*

115
Cargo.lock generated
View File

@@ -145,6 +145,9 @@ dependencies = [
"rusqlite", "rusqlite",
"serenity", "serenity",
"tokio", "tokio",
"tracing",
"tracing-error",
"tracing-subscriber",
] ]
[[package]] [[package]]
@@ -895,6 +898,15 @@ version = "0.4.22"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24"
[[package]]
name = "matchers"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558"
dependencies = [
"regex-automata 0.1.10",
]
[[package]] [[package]]
name = "memchr" name = "memchr"
version = "2.7.4" version = "2.7.4"
@@ -962,6 +974,16 @@ dependencies = [
"windows-sys 0.52.0", "windows-sys 0.52.0",
] ]
[[package]]
name = "nu-ansi-term"
version = "0.46.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84"
dependencies = [
"overload",
"winapi",
]
[[package]] [[package]]
name = "num-conv" name = "num-conv"
version = "0.1.0" version = "0.1.0"
@@ -992,6 +1014,12 @@ version = "1.20.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775"
[[package]]
name = "overload"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
[[package]] [[package]]
name = "owo-colors" name = "owo-colors"
version = "3.5.0" version = "3.5.0"
@@ -1204,8 +1232,17 @@ checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191"
dependencies = [ dependencies = [
"aho-corasick", "aho-corasick",
"memchr", "memchr",
"regex-automata", "regex-automata 0.4.8",
"regex-syntax", "regex-syntax 0.8.5",
]
[[package]]
name = "regex-automata"
version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132"
dependencies = [
"regex-syntax 0.6.29",
] ]
[[package]] [[package]]
@@ -1216,9 +1253,15 @@ checksum = "368758f23274712b504848e9d5a6f010445cc8b87a7cdb4d7cbee666c1288da3"
dependencies = [ dependencies = [
"aho-corasick", "aho-corasick",
"memchr", "memchr",
"regex-syntax", "regex-syntax 0.8.5",
] ]
[[package]]
name = "regex-syntax"
version = "0.6.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1"
[[package]] [[package]]
name = "regex-syntax" name = "regex-syntax"
version = "0.8.5" version = "0.8.5"
@@ -1874,9 +1917,9 @@ checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3"
[[package]] [[package]]
name = "tracing" name = "tracing"
version = "0.1.40" version = "0.1.41"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0"
dependencies = [ dependencies = [
"log", "log",
"pin-project-lite", "pin-project-lite",
@@ -1886,9 +1929,9 @@ dependencies = [
[[package]] [[package]]
name = "tracing-attributes" name = "tracing-attributes"
version = "0.1.27" version = "0.1.28"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@@ -1897,9 +1940,9 @@ dependencies = [
[[package]] [[package]]
name = "tracing-core" name = "tracing-core"
version = "0.1.32" version = "0.1.33"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c"
dependencies = [ dependencies = [
"once_cell", "once_cell",
"valuable", "valuable",
@@ -1907,25 +1950,43 @@ dependencies = [
[[package]] [[package]]
name = "tracing-error" name = "tracing-error"
version = "0.2.0" version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d686ec1c0f384b1277f097b2f279a2ecc11afe8c133c1aabf036a27cb4cd206e" checksum = "8b1581020d7a273442f5b45074a6a57d5757ad0a47dac0e9f0bd57b81936f3db"
dependencies = [ dependencies = [
"tracing", "tracing",
"tracing-subscriber", "tracing-subscriber",
] ]
[[package]] [[package]]
name = "tracing-subscriber" name = "tracing-log"
version = "0.3.18" version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b" checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3"
dependencies = [ dependencies = [
"sharded-slab", "log",
"thread_local", "once_cell",
"tracing-core", "tracing-core",
] ]
[[package]]
name = "tracing-subscriber"
version = "0.3.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008"
dependencies = [
"matchers",
"nu-ansi-term",
"once_cell",
"regex",
"sharded-slab",
"smallvec",
"thread_local",
"tracing",
"tracing-core",
"tracing-log",
]
[[package]] [[package]]
name = "triomphe" name = "triomphe"
version = "0.1.14" version = "0.1.14"
@@ -2214,6 +2275,22 @@ dependencies = [
"rustls-pki-types", "rustls-pki-types",
] ]
[[package]]
name = "winapi"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
dependencies = [
"winapi-i686-pc-windows-gnu",
"winapi-x86_64-pc-windows-gnu",
]
[[package]]
name = "winapi-i686-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]] [[package]]
name = "winapi-util" name = "winapi-util"
version = "0.1.9" version = "0.1.9"
@@ -2223,6 +2300,12 @@ dependencies = [
"windows-sys 0.59.0", "windows-sys 0.59.0",
] ]
[[package]]
name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]] [[package]]
name = "windows-core" name = "windows-core"
version = "0.52.0" version = "0.52.0"

View File

@@ -14,3 +14,6 @@ rand = "0.8.5"
rusqlite = { version = "0.32.1", features = ["rusqlite-macros"] } rusqlite = { version = "0.32.1", features = ["rusqlite-macros"] }
serenity = "0.12.2" serenity = "0.12.2"
tokio = { version = "1.41.0", features = ["rt", "net"] } tokio = { version = "1.41.0", features = ["rt", "net"] }
tracing = "0.1.41"
tracing-error = "0.2.1"
tracing-subscriber = { version = "0.3.19", features = ["env-filter"] }

View File

@@ -1,10 +1,16 @@
use std::path::Path; use std::{future::Future, io, path::Path};
use ::serenity::all::{ChannelId, GuildId, Mentionable, UserId};
use eyre::{Context as _, Error, OptionExt, Result}; use eyre::{Context as _, Error, OptionExt, Result};
use futures::FutureExt; use futures::{FutureExt, StreamExt, TryStreamExt};
use poise::serenity_prelude as serenity; use poise::serenity_prelude as serenity;
use rand::seq::SliceRandom; use rand::seq::SliceRandom;
use rusqlite::OptionalExtension; use rusqlite::OptionalExtension;
use tracing_subscriber::{
fmt::{self, format::FmtSpan},
layer::SubscriberExt,
EnvFilter,
};
#[derive(Copy, Clone)] #[derive(Copy, Clone)]
struct GuildData { struct GuildData {
@@ -17,13 +23,21 @@ struct GuildData {
struct Data(tokio::sync::Mutex<rusqlite::Connection>); struct Data(tokio::sync::Mutex<rusqlite::Connection>);
impl std::fmt::Debug for Data {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Data {{}}")
}
}
impl Data { impl Data {
#[tracing::instrument(skip(path))]
fn open(path: impl AsRef<Path>) -> Result<Self> { fn open(path: impl AsRef<Path>) -> Result<Self> {
let conn = rusqlite::Connection::open(path)?; let conn = rusqlite::Connection::open(path)?;
conn.execute_batch("CREATE TABLE IF NOT EXISTS guilds(guild TEXT PRIMARY KEY NOT NULL, town_square TEXT NOT NULL, cottages TEXT NOT NULL, st TEXT, currently_playing TEXT);")?; conn.execute_batch("CREATE TABLE IF NOT EXISTS guilds(guild TEXT PRIMARY KEY NOT NULL, town_square TEXT NOT NULL, cottages TEXT NOT NULL, st TEXT, currently_playing TEXT);")?;
Ok(Self(tokio::sync::Mutex::new(conn))) Ok(Self(tokio::sync::Mutex::new(conn)))
} }
#[tracing::instrument]
async fn get(&self, guild: serenity::GuildId) -> Result<GuildData> { async fn get(&self, guild: serenity::GuildId) -> Result<GuildData> {
self.0 self.0
.lock() .lock()
@@ -42,6 +56,7 @@ impl Data {
.ok_or_eyre("This bot has not been configured in this server!") .ok_or_eyre("This bot has not been configured in this server!")
} }
#[tracing::instrument]
async fn insert( async fn insert(
&self, &self,
GuildData { GuildData {
@@ -75,6 +90,7 @@ impl Data {
type Context<'a> = poise::Context<'a, Data, Error>; type Context<'a> = poise::Context<'a, Data, Error>;
#[tracing::instrument]
async fn channel_children( async fn channel_children(
ctx: &Context<'_>, ctx: &Context<'_>,
channel: serenity::ChannelId, channel: serenity::ChannelId,
@@ -90,6 +106,23 @@ async fn channel_children(
.collect()) .collect())
} }
#[tracing::instrument]
fn move_users<'a>(
ctx: Context<'a>,
guild: GuildId,
users: Vec<(ChannelId, UserId)>,
) -> impl Future<Output = Result<()>> + Send + 'a {
futures::stream::iter(users.into_iter().map(move |(channel, user)| async move {
tracing::info!(?channel, ?user, "Moving user");
guild.move_member(ctx, user, channel).await?;
tracing::info!(?channel, ?user, "Moved user");
Ok(())
}))
.buffer_unordered(10)
.try_collect::<()>()
}
#[tracing::instrument]
async fn spread( async fn spread(
ctx: Context<'_>, ctx: Context<'_>,
from: serenity::ChannelId, from: serenity::ChannelId,
@@ -99,7 +132,7 @@ async fn spread(
.guild_id() .guild_id()
.ok_or_eyre("This bot only works in servers")?; .ok_or_eyre("This bot only works in servers")?;
ctx.defer().await?; ctx.defer_ephemeral().await?;
let from = from let from = from
.to_channel(ctx) .to_channel(ctx)
@@ -107,17 +140,19 @@ async fn spread(
.guild() .guild()
.ok_or_eyre("This bot only works in servers")?; .ok_or_eyre("This bot only works in servers")?;
let mut to = channel_children(&ctx, to).await?; let mut to = channel_children(&ctx, to).await?;
let mut users = from.members(ctx)?; let mut users = from.members(ctx)?;
users.shuffle(&mut rand::thread_rng()); users.shuffle(&mut rand::thread_rng());
to.retain(|x| x.members(ctx).is_ok_and(|x| x.is_empty())); to.retain(|x| x.members(ctx).is_ok_and(|x| x.is_empty()));
to.shuffle(&mut rand::thread_rng()); to.shuffle(&mut rand::thread_rng());
futures::future::try_join_all( move_users(
ctx,
guild,
to.into_iter() to.into_iter()
.zip(&users) .zip(&users)
.map(|(channel, user)| guild.move_member(ctx, user.user.id, channel)), .map(|(channel, user)| (channel.id, user.user.id))
.collect(),
) )
.await?; .await?;
@@ -125,6 +160,7 @@ async fn spread(
Ok(()) Ok(())
} }
#[tracing::instrument]
async fn collect( async fn collect(
ctx: Context<'_>, ctx: Context<'_>,
from: serenity::ChannelId, from: serenity::ChannelId,
@@ -134,7 +170,7 @@ async fn collect(
.guild_id() .guild_id()
.ok_or_eyre("This bot only works in servers")?; .ok_or_eyre("This bot only works in servers")?;
ctx.defer().await?; ctx.defer_ephemeral().await?;
let users = channel_children(&ctx, from) let users = channel_children(&ctx, from)
.await? .await?
@@ -143,6 +179,12 @@ async fn collect(
.flatten() .flatten()
.collect::<Vec<_>>(); .collect::<Vec<_>>();
move_users(
ctx,
guild,
users.iter().map(|user| (to, user.user.id)).collect(),
)
.await?;
futures::future::try_join_all(users.iter().map(|user| guild.move_member(&ctx, user, to))) futures::future::try_join_all(users.iter().map(|user| guild.move_member(&ctx, user, to)))
.await?; .await?;
@@ -151,6 +193,7 @@ async fn collect(
} }
#[poise::command(slash_command, prefix_command, ephemeral)] #[poise::command(slash_command, prefix_command, ephemeral)]
#[tracing::instrument]
async fn dusk(ctx: Context<'_>) -> Result<()> { async fn dusk(ctx: Context<'_>) -> Result<()> {
let guild = ctx let guild = ctx
.guild_id() .guild_id()
@@ -167,6 +210,7 @@ async fn dusk(ctx: Context<'_>) -> Result<()> {
} }
#[poise::command(slash_command, prefix_command, ephemeral)] #[poise::command(slash_command, prefix_command, ephemeral)]
#[tracing::instrument]
async fn dawn(ctx: Context<'_>) -> Result<()> { async fn dawn(ctx: Context<'_>) -> Result<()> {
let guild = ctx let guild = ctx
.guild_id() .guild_id()
@@ -183,6 +227,7 @@ async fn dawn(ctx: Context<'_>) -> Result<()> {
} }
#[poise::command(slash_command, ephemeral)] #[poise::command(slash_command, ephemeral)]
#[tracing::instrument]
async fn join( async fn join(
ctx: Context<'_>, ctx: Context<'_>,
to: serenity::UserId, to: serenity::UserId,
@@ -216,54 +261,92 @@ async fn join(
} }
#[poise::command(slash_command, ephemeral)] #[poise::command(slash_command, ephemeral)]
async fn st(ctx: Context<'_>, mut spectators: Vec<serenity::UserId>) -> Result<()> { #[tracing::instrument]
async fn st(ctx: Context<'_>) -> Result<()> {
let guild = ctx let guild = ctx
.guild_id() .guild_id()
.ok_or_eyre("This bot only works in servers")?; .ok_or_eyre("This bot only works in servers")?;
let GuildData { st, .. } = ctx.data().get(guild).await?; let GuildData { st, .. } = ctx.data().get(guild).await?;
spectators.push(ctx.author().id); let sender = ctx.author().id;
futures::future::try_join_all(guild.members(&ctx, None, None).await?.into_iter().map( futures::future::try_join_all(guild.members(&ctx, None, None).await?.into_iter().map(
|member| { |member| async move {
let spectators = &spectators; match (sender == member.user.id, member.roles.contains(&st)) {
async move { (true, true) | (false, false) => Ok(()),
match (spectators.contains(&member.user.id), member.roles.contains(&st)) { (true, false) => member.add_role(&ctx, st).await,
(true, true) | (false, false) => Ok(()), (false, true) => member.remove_role(&ctx, st).await,
(true, false) => member.add_role(&ctx, st).await,
(false, true) => member.remove_role(&ctx, st).await,
}
} }
}, },
)) ))
.await?; .await?;
ctx.reply(format!("{} members given ST", spectators.len())) ctx.reply("You are now the ST").await?;
.await?;
Ok(()) Ok(())
} }
#[poise::command(slash_command, ephemeral)] #[poise::command(slash_command, ephemeral)]
async fn currently_playing(ctx: Context<'_>, players: Vec<serenity::UserId>) -> Result<()> { #[tracing::instrument]
async fn spectate(ctx: Context<'_>, player: Option<serenity::UserId>) -> Result<()> {
let guild = ctx let guild = ctx
.guild_id() .guild_id()
.ok_or_eyre("This bot only works in servers")?; .ok_or_eyre("This bot only works in servers")?;
let GuildData { let GuildData { st, .. } = ctx.data().get(guild).await?;
currently_playing, ..
} = ctx.data().get(guild).await?; let member = guild
for player in &players { .member(&ctx, player.unwrap_or(ctx.author().id))
guild
.member(&ctx, player)
.await?
.add_role(&ctx, currently_playing)
.await?;
}
ctx.reply(format!("Given {} members currently playing", players.len()))
.await?; .await?;
let msg = if member.roles.contains(&st) {
member.remove_role(&ctx, st).await?;
"no longer a spectator"
} else {
member.add_role(&ctx, st).await?;
"now a spectator"
};
ctx.reply(if player.is_some() {
format!("{} is {}", member.mention(), msg)
} else {
format!("You are {}", msg)
})
.await?;
Ok(())
}
#[poise::command(slash_command, ephemeral)]
#[tracing::instrument]
async fn currently_playing(ctx: Context<'_>, player: Option<serenity::UserId>) -> Result<()> {
let guild = ctx
.guild_id()
.ok_or_eyre("This bot only works in servers")?;
let GuildData { currently_playing, .. } = ctx.data().get(guild).await?;
let member = guild
.member(&ctx, player.unwrap_or(ctx.author().id))
.await?;
let msg = if member.roles.contains(&currently_playing) {
member.remove_role(&ctx, currently_playing).await?;
"no longer"
} else {
member.add_role(&ctx, currently_playing).await?;
"now"
};
ctx.reply(if player.is_some() {
format!("{} is {} {}", member.mention(), msg, currently_playing.mention())
} else {
format!("You are {} {}", msg, currently_playing.mention())
})
.await?;
Ok(()) Ok(())
} }
#[poise::command(slash_command, prefix_command, ephemeral)] #[poise::command(slash_command, prefix_command, ephemeral)]
#[tracing::instrument]
async fn configure( async fn configure(
ctx: Context<'_>, ctx: Context<'_>,
#[channel_types("Voice")] town_square: serenity::ChannelId, #[channel_types("Voice")] town_square: serenity::ChannelId,
@@ -289,6 +372,7 @@ async fn configure(
Ok(()) Ok(())
} }
#[tracing::instrument(skip(ctx))]
async fn register_commands( async fn register_commands(
ctx: impl AsRef<serenity::Http>, ctx: impl AsRef<serenity::Http>,
options: &poise::FrameworkOptions<Data, Error>, options: &poise::FrameworkOptions<Data, Error>,
@@ -311,8 +395,20 @@ async fn on_event(
} }
#[tokio::main(flavor = "current_thread")] #[tokio::main(flavor = "current_thread")]
#[tracing::instrument]
async fn main() -> Result<()> { async fn main() -> Result<()> {
color_eyre::install()?; color_eyre::install()?;
tracing::subscriber::set_global_default(
tracing_subscriber::registry()
.with(
fmt::layer()
.event_format(fmt::format().with_ansi(true).pretty())
.with_span_events(FmtSpan::ACTIVE)
.with_writer(io::stderr),
)
.with(EnvFilter::from_default_env())
.with(tracing_error::ErrorLayer::default()),
)?;
let token = std::fs::read_to_string( let token = std::fs::read_to_string(
std::env::var_os("DISCORD_TOKEN_FILE").ok_or_eyre("A Discord token is required")?, std::env::var_os("DISCORD_TOKEN_FILE").ok_or_eyre("A Discord token is required")?,
) )
@@ -327,6 +423,7 @@ async fn main() -> Result<()> {
dusk(), dusk(),
dawn(), dawn(),
st(), st(),
spectate(),
currently_playing(), currently_playing(),
join(), join(),
configure(), configure(),