Compare commits

..

4 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
3 changed files with 228 additions and 45 deletions

115
Cargo.lock generated
View File

@@ -145,6 +145,9 @@ dependencies = [
"rusqlite",
"serenity",
"tokio",
"tracing",
"tracing-error",
"tracing-subscriber",
]
[[package]]
@@ -895,6 +898,15 @@ version = "0.4.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
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]]
name = "memchr"
version = "2.7.4"
@@ -962,6 +974,16 @@ dependencies = [
"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]]
name = "num-conv"
version = "0.1.0"
@@ -992,6 +1014,12 @@ version = "1.20.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775"
[[package]]
name = "overload"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
[[package]]
name = "owo-colors"
version = "3.5.0"
@@ -1204,8 +1232,17 @@ checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191"
dependencies = [
"aho-corasick",
"memchr",
"regex-automata",
"regex-syntax",
"regex-automata 0.4.8",
"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]]
@@ -1216,9 +1253,15 @@ checksum = "368758f23274712b504848e9d5a6f010445cc8b87a7cdb4d7cbee666c1288da3"
dependencies = [
"aho-corasick",
"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]]
name = "regex-syntax"
version = "0.8.5"
@@ -1874,9 +1917,9 @@ checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3"
[[package]]
name = "tracing"
version = "0.1.40"
version = "0.1.41"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef"
checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0"
dependencies = [
"log",
"pin-project-lite",
@@ -1886,9 +1929,9 @@ dependencies = [
[[package]]
name = "tracing-attributes"
version = "0.1.27"
version = "0.1.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7"
checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d"
dependencies = [
"proc-macro2",
"quote",
@@ -1897,9 +1940,9 @@ dependencies = [
[[package]]
name = "tracing-core"
version = "0.1.32"
version = "0.1.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54"
checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c"
dependencies = [
"once_cell",
"valuable",
@@ -1907,25 +1950,43 @@ dependencies = [
[[package]]
name = "tracing-error"
version = "0.2.0"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d686ec1c0f384b1277f097b2f279a2ecc11afe8c133c1aabf036a27cb4cd206e"
checksum = "8b1581020d7a273442f5b45074a6a57d5757ad0a47dac0e9f0bd57b81936f3db"
dependencies = [
"tracing",
"tracing-subscriber",
]
[[package]]
name = "tracing-subscriber"
version = "0.3.18"
name = "tracing-log"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b"
checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3"
dependencies = [
"sharded-slab",
"thread_local",
"log",
"once_cell",
"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]]
name = "triomphe"
version = "0.1.14"
@@ -2214,6 +2275,22 @@ dependencies = [
"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]]
name = "winapi-util"
version = "0.1.9"
@@ -2223,6 +2300,12 @@ dependencies = [
"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]]
name = "windows-core"
version = "0.52.0"

View File

@@ -14,3 +14,6 @@ rand = "0.8.5"
rusqlite = { version = "0.32.1", features = ["rusqlite-macros"] }
serenity = "0.12.2"
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 futures::FutureExt;
use futures::{FutureExt, StreamExt, TryStreamExt};
use poise::serenity_prelude as serenity;
use rand::seq::SliceRandom;
use rusqlite::OptionalExtension;
use tracing_subscriber::{
fmt::{self, format::FmtSpan},
layer::SubscriberExt,
EnvFilter,
};
#[derive(Copy, Clone)]
struct GuildData {
@@ -17,13 +23,21 @@ struct GuildData {
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 {
#[tracing::instrument(skip(path))]
fn open(path: impl AsRef<Path>) -> Result<Self> {
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);")?;
Ok(Self(tokio::sync::Mutex::new(conn)))
}
#[tracing::instrument]
async fn get(&self, guild: serenity::GuildId) -> Result<GuildData> {
self.0
.lock()
@@ -42,6 +56,7 @@ impl Data {
.ok_or_eyre("This bot has not been configured in this server!")
}
#[tracing::instrument]
async fn insert(
&self,
GuildData {
@@ -75,6 +90,7 @@ impl Data {
type Context<'a> = poise::Context<'a, Data, Error>;
#[tracing::instrument]
async fn channel_children(
ctx: &Context<'_>,
channel: serenity::ChannelId,
@@ -90,6 +106,23 @@ async fn channel_children(
.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(
ctx: Context<'_>,
from: serenity::ChannelId,
@@ -107,17 +140,19 @@ async fn spread(
.guild()
.ok_or_eyre("This bot only works in servers")?;
let mut to = channel_children(&ctx, to).await?;
let mut users = from.members(ctx)?;
users.shuffle(&mut rand::thread_rng());
to.retain(|x| x.members(ctx).is_ok_and(|x| x.is_empty()));
to.shuffle(&mut rand::thread_rng());
futures::future::try_join_all(
move_users(
ctx,
guild,
to.into_iter()
.zip(&users)
.map(|(channel, user)| guild.move_member(ctx, user.user.id, channel)),
.map(|(channel, user)| (channel.id, user.user.id))
.collect(),
)
.await?;
@@ -125,6 +160,7 @@ async fn spread(
Ok(())
}
#[tracing::instrument]
async fn collect(
ctx: Context<'_>,
from: serenity::ChannelId,
@@ -143,6 +179,12 @@ async fn collect(
.flatten()
.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)))
.await?;
@@ -151,6 +193,7 @@ async fn collect(
}
#[poise::command(slash_command, prefix_command, ephemeral)]
#[tracing::instrument]
async fn dusk(ctx: Context<'_>) -> Result<()> {
let guild = ctx
.guild_id()
@@ -167,6 +210,7 @@ async fn dusk(ctx: Context<'_>) -> Result<()> {
}
#[poise::command(slash_command, prefix_command, ephemeral)]
#[tracing::instrument]
async fn dawn(ctx: Context<'_>) -> Result<()> {
let guild = ctx
.guild_id()
@@ -183,6 +227,7 @@ async fn dawn(ctx: Context<'_>) -> Result<()> {
}
#[poise::command(slash_command, ephemeral)]
#[tracing::instrument]
async fn join(
ctx: Context<'_>,
to: serenity::UserId,
@@ -216,54 +261,92 @@ async fn join(
}
#[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
.guild_id()
.ok_or_eyre("This bot only works in servers")?;
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(
|member| {
let spectators = &spectators;
async move {
match (spectators.contains(&member.user.id), member.roles.contains(&st)) {
(true, true) | (false, false) => Ok(()),
(true, false) => member.add_role(&ctx, st).await,
(false, true) => member.remove_role(&ctx, st).await,
}
|member| async move {
match (sender == member.user.id, member.roles.contains(&st)) {
(true, true) | (false, false) => Ok(()),
(true, false) => member.add_role(&ctx, st).await,
(false, true) => member.remove_role(&ctx, st).await,
}
},
))
.await?;
ctx.reply(format!("{} members given ST", spectators.len()))
.await?;
ctx.reply("You are now the ST").await?;
Ok(())
}
#[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
.guild_id()
.ok_or_eyre("This bot only works in servers")?;
let GuildData {
currently_playing, ..
} = ctx.data().get(guild).await?;
for player in &players {
guild
.member(&ctx, player)
.await?
.add_role(&ctx, currently_playing)
.await?;
}
ctx.reply(format!("Given {} members currently playing", players.len()))
let GuildData { st, .. } = ctx.data().get(guild).await?;
let member = guild
.member(&ctx, player.unwrap_or(ctx.author().id))
.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(())
}
#[poise::command(slash_command, prefix_command, ephemeral)]
#[tracing::instrument]
async fn configure(
ctx: Context<'_>,
#[channel_types("Voice")] town_square: serenity::ChannelId,
@@ -289,6 +372,7 @@ async fn configure(
Ok(())
}
#[tracing::instrument(skip(ctx))]
async fn register_commands(
ctx: impl AsRef<serenity::Http>,
options: &poise::FrameworkOptions<Data, Error>,
@@ -311,8 +395,20 @@ async fn on_event(
}
#[tokio::main(flavor = "current_thread")]
#[tracing::instrument]
async fn main() -> Result<()> {
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(
std::env::var_os("DISCORD_TOKEN_FILE").ok_or_eyre("A Discord token is required")?,
)
@@ -327,6 +423,7 @@ async fn main() -> Result<()> {
dusk(),
dawn(),
st(),
spectate(),
currently_playing(),
join(),
configure(),