Split youmubot-core

This commit is contained in:
Natsu Kagami 2020-02-05 17:51:14 -05:00
parent aec9cd130d
commit 84150cd82e
14 changed files with 56 additions and 38 deletions

View file

@ -0,0 +1,99 @@
use rand::{
distributions::{Distribution, Uniform},
thread_rng,
};
use serenity::{
framework::standard::{
macros::{command, group},
Args, CommandError as Error, CommandResult,
},
model::{
channel::{Channel, Message},
user::OnlineStatus,
},
utils::MessageBuilder,
};
use youmubot_prelude::*;
mod votes;
use votes::VOTE_COMMAND;
#[group]
#[description = "Community related commands. Usually comes with some sort of delays, since it involves pinging"]
#[only_in("guilds")]
#[commands(choose, vote)]
struct Community;
#[command]
#[description = r"👑 Randomly choose an active member and mention them!
Note that only online/idle users in the channel are chosen from."]
#[usage = "[title = the chosen one]"]
#[example = "the strongest in Gensokyo"]
#[bucket = "community"]
#[max_args(1)]
pub fn choose(ctx: &mut Context, m: &Message, mut args: Args) -> CommandResult {
let title = if args.is_empty() {
"the chosen one".to_owned()
} else {
args.single::<String>()?
};
let users: Result<Vec<_>, Error> = {
let guild = m.guild(&ctx).unwrap();
let guild = guild.read();
let presences = &guild.presences;
let channel = m.channel_id.to_channel(&ctx)?;
if let Channel::Guild(channel) = channel {
let channel = channel.read();
Ok(channel
.members(&ctx)?
.into_iter()
.filter(|v| !v.user.read().bot)
.map(|v| v.user_id())
.filter(|v| {
presences
.get(v)
.map(|presence| {
presence.status == OnlineStatus::Online
|| presence.status == OnlineStatus::Idle
})
.unwrap_or(false)
})
.collect())
} else {
panic!()
}
};
let users = users?;
if users.len() < 2 {
m.reply(
&ctx,
"🍰 Have this cake for yourself because no-one is here for the gods to pick.",
)?;
return Ok(());
}
let winner = {
let uniform = Uniform::from(0..users.len());
let mut rng = thread_rng();
&users[uniform.sample(&mut rng)]
};
m.channel_id.send_message(&ctx, |c| {
c.content(
MessageBuilder::new()
.push("👑 The Gensokyo gods have gathered around and decided, out of ")
.push_bold(format!("{}", users.len()))
.push(" potential prayers, ")
.push(winner.mention())
.push(" will be ")
.push_bold_safe(title)
.push(". Congrats! 🎉 🎊 🥳")
.build(),
)
})?;
Ok(())
}

View file

@ -0,0 +1,226 @@
use serenity::framework::standard::CommandError as Error;
use serenity::{
framework::standard::{macros::command, Args, CommandResult},
model::{
channel::{Message, MessageReaction, ReactionType},
id::UserId,
},
utils::MessageBuilder,
};
use std::collections::HashMap as Map;
use std::thread;
use std::time::Duration;
use youmubot_prelude::{Duration as ParseDuration, *};
#[command]
#[description = "🎌 Cast a poll upon everyone and ask them for opinions!"]
#[usage = "[duration] / [question] / [answer #1 = Yes!] / [answer #2 = No!] ..."]
#[example = "2m/How early do you get up?/Before 6/Before 7/Before 8/Fuck time"]
#[bucket = "voting"]
#[only_in(guilds)]
#[min_args(2)]
pub fn vote(ctx: &mut Context, msg: &Message, mut args: Args) -> CommandResult {
// Parse stuff first
let args = args.quoted();
let _duration = args.single::<ParseDuration>()?;
let duration = &_duration.0;
if *duration < Duration::from_secs(2 * 60) || *duration > Duration::from_secs(60 * 60 * 24) {
msg.reply(ctx, format!("😒 Invalid duration ({}). The voting time should be between **2 minutes** and **1 day**.", _duration))?;
return Ok(());
}
let question = args.single::<String>()?;
let choices = if args.is_empty() {
vec![("😍", "Yes! 😍".to_owned()), ("🤢", "No! 🤢".to_owned())]
} else {
let choices: Vec<_> = args.iter().map(|v| v.unwrap()).collect();
if choices.len() < 2 {
// Where are the choices?
msg.reply(
ctx,
"😒 Can't have a nice voting session if you only have one choice.",
)?;
return Ok(());
}
if choices.len() > MAX_CHOICES {
// Too many choices!
msg.reply(
ctx,
format!(
"😵 Too many choices... We only support {} choices at the moment!",
MAX_CHOICES
),
)?;
return Ok(());
}
pick_n_reactions(choices.len())?
.into_iter()
.zip(choices.into_iter())
.collect()
};
let fields: Vec<_> = {
choices
.iter()
.map(|(choice, reaction)| {
(
MessageBuilder::new().push_bold_safe(choice).build(),
format!("React with {}", reaction),
true,
)
})
.collect()
};
// Ok... now we post up a nice voting panel.
let channel = msg.channel_id;
let author = &msg.author;
let panel = channel.send_message(&ctx, |c| {
c.content("@here").embed(|e| {
e.author(|au| {
au.icon_url(author.avatar_url().unwrap_or("".to_owned()))
.name(&author.name)
})
.title(format!("You have {} to vote!", _duration))
.thumbnail("https://images-ext-2.discordapp.net/external/BK7injOyt4XT8yNfbCDV4mAkwoRy49YPfq-3IwCc_9M/http/cdn.i.ntere.st/p/9197498/image")
.description(MessageBuilder::new().push_bold_line_safe(&question).push("\nThis question was asked by ").push(author.mention()))
.fields(fields.into_iter())
})
})?;
msg.delete(&ctx)?;
// React on all the choices
choices
.iter()
.try_for_each(|(v, _)| panel.react(&ctx, *v))?;
// Start sleeping
thread::sleep(*duration);
let result = collect_reactions(ctx, &panel, &choices)?;
if result.len() == 0 {
msg.reply(
&ctx,
MessageBuilder::new()
.push("no one answer your question ")
.push_bold_safe(&question)
.push(", sorry 😭")
.build(),
)?;
} else {
channel.send_message(&ctx, |c| {
c.content({
let mut content = MessageBuilder::new();
content
.push("@here, ")
.push(author.mention())
.push(" previously asked ")
.push_bold_safe(&question)
.push(", and here are the results!");
result.iter().for_each(|(choice, votes)| {
content
.push("\n - ")
.push_bold(format!("{}", votes.len()))
.push(" voted for ")
.push_bold_safe(choice)
.push(": ")
.push(
votes
.iter()
.map(|v| v.mention())
.collect::<Vec<_>>()
.join(", "),
);
});
content.build()
})
})?;
}
panel.delete(&ctx)?;
Ok(())
// unimplemented!();
}
// Collect reactions and store them as a map from choice to
fn collect_reactions<'a>(
ctx: &mut Context,
msg: &Message,
choices: &'a [(&'static str, String)],
) -> Result<Vec<(&'a str, Vec<UserId>)>, Error> {
// Get a brand new version of the Message
let reactions = msg.channel_id.message(&ctx, msg.id)?.reactions;
let reaction_to_choice: Map<_, _> = choices.into_iter().map(|r| (r.0, &r.1)).collect();
let mut vec: Vec<(&str, Vec<UserId>)> = Vec::new();
reactions
.into_iter()
.filter_map(|r| {
if let ReactionType::Unicode(ref v) = r.reaction_type {
reaction_to_choice
.get(&&v[..])
.cloned()
.filter(|_| r.count > 1)
.map(|choice| (r.clone(), choice))
} else {
None
}
})
.try_for_each(|(r, choice)| -> Result<_, Error> {
let users = collect_reaction_users(ctx, &msg, &r)?;
vec.push((choice, users));
Ok(())
})?;
vec.sort_by(|(_, b): &(_, Vec<_>), (_, d)| d.len().cmp(&b.len()));
Ok(vec)
}
fn collect_reaction_users(
ctx: &mut Context,
msg: &Message,
reaction: &MessageReaction,
) -> Result<Vec<UserId>, Error> {
let mut res = Vec::with_capacity(reaction.count as usize);
(0..reaction.count)
.step_by(100)
.try_for_each(|_| -> Result<_, Error> {
let user_ids = msg
.reaction_users(
&ctx,
reaction.reaction_type.clone(),
Some(100),
res.last().cloned(),
)?
.into_iter()
.map(|i| i.id);
res.extend(user_ids);
Ok(())
})?;
Ok(res)
}
// Pick a set of random n reactions!
fn pick_n_reactions(n: usize) -> Result<Vec<&'static str>, Error> {
use rand::seq::SliceRandom;
if n > MAX_CHOICES {
Err(Error::from("Too many options"))
} else {
let mut rand = rand::thread_rng();
Ok(REACTIONS
.choose_multiple(&mut rand, n)
.map(|v| *v)
.collect())
}
}
const MAX_CHOICES: usize = 15;
// All the defined reactions.
const REACTIONS: [&'static str; 90] = [
"😀", "😁", "😂", "🤣", "😃", "😄", "😅", "😆", "😉", "😊", "😋", "😎", "😍", "😘", "🥰", "😗",
"😙", "😚", "☺️", "🙂", "🤗", "🤩", "🤔", "🤨", "😐", "😑", "😶", "🙄", "😏", "😣", "😥", "😮",
"🤐", "😯", "😪", "😫", "😴", "😌", "😛", "😜", "😝", "🤤", "😒", "😓", "😔", "😕", "🙃", "🤑",
"😲", "☹️", "🙁", "😖", "😞", "😟", "😤", "😢", "😭", "😦", "😧", "😨", "😩", "🤯", "😬", "😰",
"😱", "🥵", "🥶", "😳", "🤪", "😵", "😡", "😠", "🤬", "😷", "🤒", "🤕", "🤢", "🤮", "🤧", "😇",
"🤠", "🤡", "🥳", "🥴", "🥺", "🤥", "🤫", "🤭", "🧐", "🤓",
];
// Assertions
static_assertions::const_assert!(MAX_CHOICES <= REACTIONS.len());