aboutsummaryrefslogtreecommitdiff
path: root/src/util/mod.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/util/mod.rs')
-rw-r--r--src/util/mod.rs120
1 files changed, 104 insertions, 16 deletions
diff --git a/src/util/mod.rs b/src/util/mod.rs
index 9355e48..38f4e55 100644
--- a/src/util/mod.rs
+++ b/src/util/mod.rs
@@ -1,6 +1,19 @@
-use std::process::Stdio;
+use std::{
+ cmp::max_by,
+ collections::HashMap,
+ process::{
+ id,
+ Stdio,
+ },
+};
+use anyhow::anyhow;
use chrono::Duration;
+use diesel::row::NamedRow;
+use fnv::{
+ FnvHashMap,
+ FnvHashSet,
+};
use grate::tracing;
use lazy_static::lazy_static;
use poise::{
@@ -13,10 +26,14 @@ use regex::{
};
use serenity::{
all::{
+ ChannelType,
CreateMessage,
+ GuildId,
+ GuildRef,
Message,
Reaction,
ReactionType,
+ VoiceState,
},
client::Context,
model::{
@@ -27,6 +44,7 @@ use serenity::{
permissions::Permissions,
},
};
+use tap::Pipe;
use url::Url;
use crate::{
@@ -49,29 +67,25 @@ pub async fn currently_playing(ctx: PoiseContext<'_>) -> bool {
call.queue().current().is_some()
}
-pub async fn users_listening(ctx: &Context) -> anyhow::Result<bool> {
- let channel = CONFIG.discord.voice_channel().to_channel(&ctx).await?;
+pub async fn users_listening(ctx: PoiseContext<'_>) -> anyhow::Result<bool> {
+ let Some(channel) = best_voice_channel(ctx) else {
+ return Ok(false);
+ };
- let res = channel
- .guild()
- .and_then(|ch| ch.guild(&ctx))
- .map(|g| {
- g.voice_states
- .iter()
- .any(|(_, state)| state.channel_id == Some(CONFIG.discord.voice_channel()))
- })
- .unwrap_or(false);
+ let Some(guild) = ctx.guild() else {
+ return Ok(false);
+ };
- Ok(res)
+ guild.voice_states.iter().any(|(_, state)| state.channel_id == Some(channel)).pipe(Ok)
}
#[inline]
pub fn msg<U, E>(ctx: poise::Context<'_, U, E>) -> Option<&Message> {
match ctx {
poise::Context::Prefix(poise::PrefixContext {
- msg,
- ..
- }) => Some(msg),
+ msg,
+ ..
+ }) => Some(msg),
_ => None,
}
}
@@ -154,6 +168,80 @@ pub async fn unreact(
Ok(())
}
+#[inline]
+pub fn guild_id(ctx: PoiseContext<'_>) -> anyhow::Result<GuildId> {
+ ctx.guild_id().ok_or_else(|| anyhow::anyhow!("not in guild"))
+}
+
+#[inline]
+pub fn guild(ctx: PoiseContext<'_>) -> anyhow::Result<GuildRef<'_>> {
+ ctx.guild().ok_or_else(|| anyhow::anyhow!("not in guild"))
+}
+
+#[inline]
+pub fn author_voice_state(ctx: PoiseContext<'_>) -> Option<(VoiceState, GuildRef)> {
+ let guild = ctx.guild()?;
+ let caller_voice = guild.voice_states.get(&ctx.author().id)?.clone();
+
+ Some((caller_voice, guild))
+}
+
+#[inline]
+pub fn author_voice_channel(ctx: PoiseContext<'_>) -> Option<ChannelId> {
+ let (vs, _guild) = author_voice_state(ctx)?;
+ vs.channel_id
+}
+
+pub fn voice_states_by_channel(ctx: PoiseContext<'_>) -> HashMap<ChannelId, Vec<VoiceState>> {
+ let Some(guild) = ctx.guild() else {
+ return Default::default();
+ };
+
+ guild
+ .voice_states
+ .values()
+ .cloned()
+ .filter_map(|x| {
+ let id = x.channel_id?;
+ Some((id, x))
+ })
+ .fold(HashMap::new(), |mut acc, (id, state)| {
+ acc.entry(id).or_insert_with(|| vec![]).push(state);
+
+ acc
+ })
+}
+
+/// Select the most relevant voice channel for a given poise context.
+///
+/// - If the message's author is in a voice channel, use that.
+/// - If not, pick the most populated channel (channel age tiebreaks).
+pub fn best_voice_channel(ctx: PoiseContext<'_>) -> Option<ChannelId> {
+ if let Some(channel) = author_voice_channel(ctx) {
+ return Some(channel);
+ }
+
+ let voice_states = voice_states_by_channel(ctx);
+ let max_pop = voice_states.iter().map(|(_, states)| states.len()).max();
+
+ let matching_channels = voice_states
+ .iter()
+ .filter_map(|(&channel, state)| {
+ if state.len() == max_pop? {
+ return Some(channel);
+ }
+
+ None
+ })
+ .collect::<Vec<_>>();
+
+ if matching_channels.len() == 1 {
+ return matching_channels.first().cloned();
+ }
+
+ matching_channels.into_iter().min()
+}
+
pub async fn send_result(
ctx: &Context,
channel: ChannelId,