1use crate::commands::SlashCommand;
2use crate::config::Config;
3use serenity::all::{
4 ChannelId, CommandInteraction, CommandOptionType, Context, CreateCommand, CreateCommandOption,
5 CreateInteractionResponse, CreateInteractionResponseMessage, UserId,
6};
7use tracing::{error, info, warn};
8
9pub struct VotekickCommand;
11
12#[serenity::async_trait]
13impl SlashCommand for VotekickCommand {
14 fn name(&self) -> &'static str {
15 "votekick"
16 }
17
18 fn register(&self) -> CreateCommand {
19 CreateCommand::new(self.name())
20 .description("Start a votekick poll against a user")
21 .add_option(
22 CreateCommandOption::new(CommandOptionType::User, "user", "The user to votekick")
23 .required(true),
24 )
25 .add_option(
26 CreateCommandOption::new(
27 CommandOptionType::Integer,
28 "duration",
29 "Poll duration in seconds",
30 )
31 .required(false),
32 )
33 }
34
35 async fn run(&self, ctx: Context, command: CommandInteraction, config: Config) {
36 if let Err(e) = self.run_internal(&ctx, &command, &config).await {
37 error!("Votekick error: {}", e);
38 }
39 }
40}
41
42impl VotekickCommand {
43 async fn run_internal(
46 &self,
47 ctx: &Context,
48 command: &CommandInteraction,
49 config: &Config,
50 ) -> anyhow::Result<()> {
51 let user = &command.user;
52 let guild_id = command
53 .guild_id
54 .map(|id| id.to_string())
55 .unwrap_or_else(|| "DM".to_string());
56
57 info!(
58 "Command 'votekick' invoked by {} ({}) in {}",
59 user.name, user.id, guild_id,
60 );
61
62 let Some(guild_id) = command.guild_id else {
63 warn!("votekick used in DM by {}", user.name);
64 self.send_error(ctx, command, "This command can only be used in a server.")
65 .await;
66 return Ok(());
67 };
68
69 let target_user_id = command
70 .data
71 .options
72 .first()
73 .and_then(|opt| opt.value.as_user_id())
74 .expect("User option is required");
75
76 let duration = command
77 .data
78 .options
79 .get(1)
80 .and_then(|opt| opt.value.as_i64())
81 .map(|v| {
82 v.clamp(
83 config.min_votekick_duration as i64,
84 config.max_votekick_duration as i64,
85 ) as u64
86 })
87 .unwrap_or(config.default_votekick_duration);
88
89 let user_channel_id = match self.get_user_voice_channel(ctx, guild_id, user.id) {
90 Some(cid) => cid,
91 None => {
92 warn!("{} tried votekick but is not in a voice channel", user.name);
93 self.send_error(
94 ctx,
95 command,
96 "You must be in a voice channel to use this command.",
97 )
98 .await;
99 return Ok(());
100 }
101 };
102
103 let (target_in_same_channel, target_screensharing) =
104 self.check_target_user(ctx, guild_id, target_user_id, user_channel_id);
105
106 if !target_in_same_channel {
107 warn!(
108 "Target user {} is not in the same voice channel as {}",
109 target_user_id, user.name
110 );
111 self.send_error(
112 ctx,
113 command,
114 "The target user must be in the same voice channel as you.",
115 )
116 .await;
117 return Ok(());
118 }
119
120 if !target_screensharing {
121 warn!("Target user {} is not screensharing", target_user_id);
122 self.send_error(
123 ctx,
124 command,
125 "The target user must be screensharing to start a votekick.",
126 )
127 .await;
128 return Ok(());
129 }
130
131 info!(
132 "Votekick starting by {} targeting {} in channel {} (duration: {}s)",
133 user.name, target_user_id, user_channel_id, duration
134 );
135
136 crate::polls::votekick::start_votekick(
137 ctx,
138 command,
139 target_user_id,
140 user_channel_id,
141 duration,
142 )
143 .await;
144
145 Ok(())
146 }
147
148 fn get_user_voice_channel(
150 &self,
151 ctx: &Context,
152 guild_id: serenity::all::GuildId,
153 user_id: UserId,
154 ) -> Option<ChannelId> {
155 let guild = ctx.cache.guild(guild_id)?;
156 let vs = guild.voice_states.get(&user_id)?;
157 vs.channel_id
158 }
159
160 fn check_target_user(
163 &self,
164 ctx: &Context,
165 guild_id: serenity::all::GuildId,
166 target_user_id: UserId,
167 user_channel_id: ChannelId,
168 ) -> (bool, bool) {
169 let Some(guild) = ctx.cache.guild(guild_id) else {
170 return (false, false);
171 };
172 let Some(vs) = guild.voice_states.get(&target_user_id) else {
173 return (false, false);
174 };
175 let in_same = vs.channel_id == Some(user_channel_id);
176 let screensharing = vs.self_stream.unwrap_or(false);
177 (in_same, screensharing)
178 }
179
180 async fn send_error(&self, ctx: &Context, command: &CommandInteraction, message: &str) {
182 if let Err(e) = command
183 .create_response(
184 &ctx.http,
185 CreateInteractionResponse::Message(
186 CreateInteractionResponseMessage::new()
187 .content(message)
188 .ephemeral(true),
189 ),
190 )
191 .await
192 {
193 error!("Failed to send error response: {}", e);
194 }
195 }
196}