mirror of
https://github.com/natsukagami/youmubot.git
synced 2025-05-24 09:10:49 +00:00
Split youmubot-core
This commit is contained in:
parent
aec9cd130d
commit
84150cd82e
14 changed files with 56 additions and 38 deletions
108
youmubot-core/src/admin/mod.rs
Normal file
108
youmubot-core/src/admin/mod.rs
Normal file
|
@ -0,0 +1,108 @@
|
|||
use serenity::{
|
||||
framework::standard::{
|
||||
macros::{command, group},
|
||||
Args, CommandResult,
|
||||
},
|
||||
model::{
|
||||
channel::{Channel, Message},
|
||||
id::UserId,
|
||||
},
|
||||
};
|
||||
use soft_ban::{SOFT_BAN_COMMAND, SOFT_BAN_INIT_COMMAND};
|
||||
use std::{thread::sleep, time::Duration};
|
||||
use youmubot_prelude::*;
|
||||
|
||||
mod soft_ban;
|
||||
pub use soft_ban::watch_soft_bans;
|
||||
|
||||
#[group]
|
||||
#[description = "Administrative commands for the server."]
|
||||
#[commands(clean, ban, kick, soft_ban, soft_ban_init)]
|
||||
struct Admin;
|
||||
|
||||
#[command]
|
||||
#[aliases("cleanall")]
|
||||
#[required_permissions(MANAGE_MESSAGES)]
|
||||
#[description = "Clean at most X latest messages from the current channel (only clean Youmu's messages in DMs). Defaults to 10."]
|
||||
#[usage = "clean 50"]
|
||||
#[min_args(0)]
|
||||
#[max_args(1)]
|
||||
fn clean(ctx: &mut Context, msg: &Message, mut args: Args) -> CommandResult {
|
||||
let limit = args.single().unwrap_or(10);
|
||||
let messages = msg
|
||||
.channel_id
|
||||
.messages(&ctx.http, |b| b.before(msg.id).limit(limit))?;
|
||||
let channel = msg.channel_id.to_channel(&ctx)?;
|
||||
match &channel {
|
||||
Channel::Private(_) | Channel::Group(_) => {
|
||||
let self_id = ctx.http.get_current_application_info()?.id;
|
||||
messages
|
||||
.into_iter()
|
||||
.filter(|v| v.author.id == self_id)
|
||||
.try_for_each(|m| m.delete(&ctx))?;
|
||||
}
|
||||
_ => {
|
||||
msg.channel_id
|
||||
.delete_messages(&ctx.http, messages.into_iter())?;
|
||||
}
|
||||
};
|
||||
msg.react(&ctx, "🌋")?;
|
||||
if let Channel::Guild(_) = &channel {
|
||||
sleep(Duration::from_secs(2));
|
||||
msg.delete(&ctx)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[command]
|
||||
#[required_permissions(ADMINISTRATOR)]
|
||||
#[description = "Ban an user with a certain reason."]
|
||||
#[usage = "ban user#1234 spam"]
|
||||
#[min_args(1)]
|
||||
#[max_args(2)]
|
||||
#[only_in("guilds")]
|
||||
fn ban(ctx: &mut Context, msg: &Message, mut args: Args) -> CommandResult {
|
||||
let user = args.single::<UserId>()?.to_user(&ctx)?;
|
||||
let reason = args
|
||||
.remains()
|
||||
.map(|v| format!("`{}`", v))
|
||||
.unwrap_or("no provided reason".to_owned());
|
||||
|
||||
msg.reply(
|
||||
&ctx,
|
||||
format!("🔨 Banning user {} for reason `{}`.", user.tag(), reason),
|
||||
)?;
|
||||
|
||||
msg.guild_id
|
||||
.ok_or("Can't get guild from message?")? // we had a contract
|
||||
.ban(&ctx.http, user, &reason)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[command]
|
||||
#[required_permissions(ADMINISTRATOR)]
|
||||
#[description = "Kick an user with a certain reason."]
|
||||
#[usage = "kick user#1234 spam"]
|
||||
#[min_args(1)]
|
||||
#[max_args(2)]
|
||||
#[only_in("guilds")]
|
||||
fn kick(ctx: &mut Context, msg: &Message, mut args: Args) -> CommandResult {
|
||||
let user = args.single::<UserId>()?.to_user(&ctx)?;
|
||||
let reason = args
|
||||
.remains()
|
||||
.map(|v| format!("`{}`", v))
|
||||
.unwrap_or("no provided reason".to_owned());
|
||||
|
||||
msg.reply(
|
||||
&ctx,
|
||||
format!("🔫 Kicking user {} for {}.", user.tag(), reason),
|
||||
)?;
|
||||
|
||||
msg.guild_id
|
||||
.ok_or("Can't get guild from message?")? // we had a contract
|
||||
.ban(&ctx.http, user, &reason)?;
|
||||
|
||||
Ok(())
|
||||
}
|
164
youmubot-core/src/admin/soft_ban.rs
Normal file
164
youmubot-core/src/admin/soft_ban.rs
Normal file
|
@ -0,0 +1,164 @@
|
|||
use crate::db::{ServerSoftBans, SoftBans};
|
||||
use chrono::offset::Utc;
|
||||
use serenity::{
|
||||
framework::standard::{macros::command, Args, CommandError as Error, CommandResult},
|
||||
model::{
|
||||
channel::Message,
|
||||
id::{RoleId, UserId},
|
||||
},
|
||||
};
|
||||
use std::cmp::max;
|
||||
use youmubot_prelude::*;
|
||||
|
||||
#[command]
|
||||
#[required_permissions(ADMINISTRATOR)]
|
||||
#[description = "Soft-ban an user, might be with a certain amount of time. Re-banning an user removes the ban itself."]
|
||||
#[usage = "user#1234 [time]"]
|
||||
#[example = "user#1234 5s"]
|
||||
#[min_args(1)]
|
||||
#[max_args(2)]
|
||||
#[only_in("guilds")]
|
||||
pub fn soft_ban(ctx: &mut Context, msg: &Message, mut args: Args) -> CommandResult {
|
||||
let user = args.single::<UserId>()?.to_user(&ctx)?;
|
||||
let duration = if args.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(
|
||||
args.single::<args::Duration>()
|
||||
.map_err(|e| Error::from(&format!("{:?}", e)))?,
|
||||
)
|
||||
};
|
||||
let guild = msg.guild_id.ok_or(Error::from("Command is guild only"))?;
|
||||
|
||||
let db = SoftBans::open(&*ctx.data.read());
|
||||
let mut db = db.borrow_mut()?;
|
||||
let mut server_ban = db.get_mut(&guild).and_then(|v| match v {
|
||||
ServerSoftBans::Unimplemented => None,
|
||||
ServerSoftBans::Implemented(ref mut v) => Some(v),
|
||||
});
|
||||
|
||||
match server_ban {
|
||||
None => {
|
||||
println!("get here");
|
||||
msg.reply(&ctx, format!("⚠ This server has not enabled the soft-ban feature. Check out `y!a soft-ban-init`."))?;
|
||||
}
|
||||
Some(ref mut server_ban) => {
|
||||
let mut member = guild.member(&ctx, &user)?;
|
||||
match duration {
|
||||
None if member.roles.contains(&server_ban.role) => {
|
||||
msg.reply(&ctx, format!("⛓ Lifting soft-ban for user {}.", user.tag()))?;
|
||||
member.remove_role(&ctx, server_ban.role)?;
|
||||
return Ok(());
|
||||
}
|
||||
None => {
|
||||
msg.reply(&ctx, format!("⛓ Soft-banning user {}.", user.tag()))?;
|
||||
}
|
||||
Some(v) => {
|
||||
let until = Utc::now() + chrono::Duration::from_std(v.0)?;
|
||||
let until = server_ban
|
||||
.periodical_bans
|
||||
.entry(user.id)
|
||||
.and_modify(|v| *v = max(*v, until))
|
||||
.or_insert(until);
|
||||
msg.reply(
|
||||
&ctx,
|
||||
format!("⛓ Soft-banning user {} until {}.", user.tag(), until),
|
||||
)?;
|
||||
}
|
||||
}
|
||||
member.add_role(&ctx, server_ban.role)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[command]
|
||||
#[required_permissions(ADMINISTRATOR)]
|
||||
#[description = "Sets up the soft-ban command. This command can only be run once.\nThe soft-ban command assigns a role, temporarily, to a user."]
|
||||
#[usage = "{soft_ban_role_id}"]
|
||||
#[num_args(1)]
|
||||
#[only_in("guilds")]
|
||||
pub fn soft_ban_init(ctx: &mut Context, msg: &Message, mut args: Args) -> CommandResult {
|
||||
let role_id = args.single::<RoleId>()?;
|
||||
let guild = msg.guild(&ctx).ok_or(Error::from("Guild-only command"))?;
|
||||
let guild = guild.read();
|
||||
// Check whether the role_id is the one we wanted
|
||||
if !guild.roles.contains_key(&role_id) {
|
||||
return Err(Error::from(format!(
|
||||
"{} is not a role in this server.",
|
||||
role_id
|
||||
)));
|
||||
}
|
||||
// Check if we already set up
|
||||
let db = SoftBans::open(&*ctx.data.read());
|
||||
let mut db = db.borrow_mut()?;
|
||||
let server = db
|
||||
.get(&guild.id)
|
||||
.map(|v| match v {
|
||||
ServerSoftBans::Unimplemented => false,
|
||||
_ => true,
|
||||
})
|
||||
.unwrap_or(false);
|
||||
|
||||
if !server {
|
||||
db.insert(guild.id, ServerSoftBans::new_implemented(role_id));
|
||||
msg.react(&ctx, "👌")?;
|
||||
Ok(())
|
||||
} else {
|
||||
Err(Error::from("Server already set up soft-bans."))
|
||||
}
|
||||
}
|
||||
|
||||
// Watch the soft bans.
|
||||
pub fn watch_soft_bans(client: &serenity::Client) -> impl FnOnce() -> () + 'static {
|
||||
let cache_http = {
|
||||
let cache_http = client.cache_and_http.clone();
|
||||
let cache: serenity::cache::CacheRwLock = cache_http.cache.clone().into();
|
||||
(cache, cache_http.http.clone())
|
||||
};
|
||||
let data = client.data.clone();
|
||||
return move || {
|
||||
let cache_http = (&cache_http.0, &*cache_http.1);
|
||||
loop {
|
||||
// Scope so that locks are released
|
||||
{
|
||||
// Poll the data for any changes.
|
||||
let db = data.read();
|
||||
let db = SoftBans::open(&*db);
|
||||
let mut db = db.borrow_mut().expect("Borrowable");
|
||||
let now = Utc::now();
|
||||
for (server_id, soft_bans) in db.iter_mut() {
|
||||
let server_name: String = match server_id.to_partial_guild(cache_http) {
|
||||
Err(_) => continue,
|
||||
Ok(v) => v.name,
|
||||
};
|
||||
if let ServerSoftBans::Implemented(ref mut bans) = soft_bans {
|
||||
let to_remove: Vec<_> = bans
|
||||
.periodical_bans
|
||||
.iter()
|
||||
.filter_map(|(user, time)| if time <= &now { Some(user) } else { None })
|
||||
.cloned()
|
||||
.collect();
|
||||
for user_id in to_remove {
|
||||
server_id
|
||||
.member(cache_http, user_id)
|
||||
.and_then(|mut m| {
|
||||
println!(
|
||||
"Soft-ban for `{}` in server `{}` unlifted.",
|
||||
m.user.read().name,
|
||||
server_name
|
||||
);
|
||||
m.remove_role(cache_http, bans.role)
|
||||
})
|
||||
.unwrap_or(());
|
||||
bans.periodical_bans.remove(&user_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Sleep the thread for a minute
|
||||
std::thread::sleep(std::time::Duration::from_secs(60))
|
||||
}
|
||||
};
|
||||
}
|
99
youmubot-core/src/community/mod.rs
Normal file
99
youmubot-core/src/community/mod.rs
Normal 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(())
|
||||
}
|
226
youmubot-core/src/community/votes.rs
Normal file
226
youmubot-core/src/community/votes.rs
Normal 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());
|
38
youmubot-core/src/db.rs
Normal file
38
youmubot-core/src/db.rs
Normal file
|
@ -0,0 +1,38 @@
|
|||
use chrono::{DateTime, Utc};
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serenity::{
|
||||
model::id::{RoleId, UserId},
|
||||
};
|
||||
use std::collections::HashMap;
|
||||
use youmubot_db::{GuildMap, DB};
|
||||
|
||||
/// A list of SoftBans for all servers.
|
||||
pub type SoftBans = DB<GuildMap<ServerSoftBans>>;
|
||||
|
||||
/// For the admin commands:
|
||||
/// - Each server might have a `soft ban` role implemented.
|
||||
/// - We allow periodical `soft ban` applications.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub enum ServerSoftBans {
|
||||
Implemented(ImplementedSoftBans),
|
||||
Unimplemented,
|
||||
}
|
||||
|
||||
impl ServerSoftBans {
|
||||
// Create a new, implemented role.
|
||||
pub fn new_implemented(role: RoleId) -> ServerSoftBans {
|
||||
ServerSoftBans::Implemented(ImplementedSoftBans {
|
||||
role,
|
||||
periodical_bans: HashMap::new(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct ImplementedSoftBans {
|
||||
/// The soft-ban role.
|
||||
pub role: RoleId,
|
||||
/// List of all to-unban people.
|
||||
pub periodical_bans: HashMap<UserId, DateTime<Utc>>,
|
||||
}
|
102
youmubot-core/src/fun/images.rs
Normal file
102
youmubot-core/src/fun/images.rs
Normal file
|
@ -0,0 +1,102 @@
|
|||
use serde::Deserialize;
|
||||
use serenity::framework::standard::CommandError as Error;
|
||||
use serenity::{
|
||||
framework::standard::{
|
||||
macros::{check, command},
|
||||
Args, CheckResult, CommandOptions, CommandResult, Reason,
|
||||
},
|
||||
model::channel::{Channel, Message},
|
||||
};
|
||||
use std::string::ToString;
|
||||
use youmubot_prelude::*;
|
||||
|
||||
#[command]
|
||||
#[checks(nsfw)]
|
||||
#[description = "🖼️ Find an image with a given tag on Danbooru[nsfw]!"]
|
||||
#[min_args(1)]
|
||||
#[bucket("images")]
|
||||
pub fn nsfw(ctx: &mut Context, msg: &Message, args: Args) -> CommandResult {
|
||||
message_command(ctx, msg, args, Rating::Explicit)
|
||||
}
|
||||
|
||||
#[command]
|
||||
#[description = "🖼️ Find an image with a given tag on Danbooru[safe]!"]
|
||||
#[min_args(1)]
|
||||
#[bucket("images")]
|
||||
pub fn image(ctx: &mut Context, msg: &Message, args: Args) -> CommandResult {
|
||||
message_command(ctx, msg, args, Rating::Safe)
|
||||
}
|
||||
|
||||
#[check]
|
||||
#[name = "nsfw"]
|
||||
fn nsfw_check(ctx: &mut Context, msg: &Message, _: &mut Args, _: &CommandOptions) -> CheckResult {
|
||||
let channel = msg.channel_id.to_channel(&ctx).unwrap();
|
||||
if !(match channel {
|
||||
Channel::Guild(guild_channel) => guild_channel.read().nsfw,
|
||||
_ => true,
|
||||
}) {
|
||||
CheckResult::Failure(Reason::User("😣 YOU FREAKING PERVERT!!!".to_owned()))
|
||||
} else {
|
||||
CheckResult::Success
|
||||
}
|
||||
}
|
||||
|
||||
fn message_command(ctx: &mut Context, msg: &Message, args: Args, rating: Rating) -> CommandResult {
|
||||
let tags = args.remains().unwrap_or("touhou");
|
||||
let http = ctx.data.get_cloned::<HTTPClient>();
|
||||
let image = get_image(&http, rating, tags)?;
|
||||
match image {
|
||||
None => msg.reply(&ctx, "🖼️ No image found...\n💡 Tip: In danbooru, character names follow Japanese standards (last name before first name), so **Hakurei Reimu** might give you an image while **Reimu Hakurei** won't."),
|
||||
Some(url) => msg.reply(
|
||||
&ctx,
|
||||
format!("🖼️ Here's the image you requested!\n\n{}", url),
|
||||
),
|
||||
}?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Gets an image URL.
|
||||
fn get_image(
|
||||
client: &<HTTPClient as TypeMapKey>::Value,
|
||||
rating: Rating,
|
||||
tags: &str,
|
||||
) -> Result<Option<String>, Error> {
|
||||
// Fix the tags: change whitespaces to +
|
||||
let tags = tags.split_whitespace().collect::<Vec<_>>().join("_");
|
||||
let req = client
|
||||
.get(&format!(
|
||||
"https://danbooru.donmai.us/posts.json?tags=rating:{}+{}",
|
||||
rating.to_string(),
|
||||
tags
|
||||
))
|
||||
.query(&[("limit", "1"), ("random", "true")])
|
||||
.build()?;
|
||||
println!("{:?}", req.url());
|
||||
let response: Vec<PostResponse> = client.execute(req)?.json()?;
|
||||
Ok(response
|
||||
.into_iter()
|
||||
.next()
|
||||
.map(|v| format!("https://danbooru.donmai.us/posts/{}", v.id)))
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
struct PostResponse {
|
||||
id: u64,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
enum Rating {
|
||||
Explicit,
|
||||
Safe,
|
||||
}
|
||||
|
||||
impl ToString for Rating {
|
||||
fn to_string(&self) -> String {
|
||||
use Rating::*;
|
||||
match self {
|
||||
Explicit => "explicit",
|
||||
Safe => "safe",
|
||||
}
|
||||
.to_owned()
|
||||
}
|
||||
}
|
178
youmubot-core/src/fun/mod.rs
Normal file
178
youmubot-core/src/fun/mod.rs
Normal file
|
@ -0,0 +1,178 @@
|
|||
use rand::{
|
||||
distributions::{Distribution, Uniform},
|
||||
thread_rng,
|
||||
};
|
||||
use serenity::{
|
||||
framework::standard::{
|
||||
macros::{command, group},
|
||||
Args, CommandResult,
|
||||
},
|
||||
model::{channel::Message, id::UserId},
|
||||
utils::MessageBuilder,
|
||||
};
|
||||
use youmubot_prelude::*;
|
||||
|
||||
mod images;
|
||||
mod names;
|
||||
|
||||
use images::*;
|
||||
|
||||
#[group]
|
||||
#[description = "Random commands"]
|
||||
#[commands(roll, pick, name, image, nsfw)]
|
||||
struct Fun;
|
||||
|
||||
#[command]
|
||||
#[description = "🎲 Rolls a dice that gives you a random number."]
|
||||
#[min_args(0)]
|
||||
#[max_args(2)]
|
||||
#[usage = "[max-dice-faces = 6] / [message]"]
|
||||
#[example = "100 / What's my score?"]
|
||||
fn roll(ctx: &mut Context, msg: &Message, mut args: Args) -> CommandResult {
|
||||
let dice = if args.is_empty() {
|
||||
6
|
||||
} else {
|
||||
args.single::<u64>()?
|
||||
};
|
||||
|
||||
if dice == 0 {
|
||||
msg.reply(&ctx, "Give me a dice with 0 faces, what do you expect 😒")?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let result = {
|
||||
let dice_rng = Uniform::from(1..=dice);
|
||||
let mut rng = thread_rng();
|
||||
dice_rng.sample(&mut rng)
|
||||
};
|
||||
|
||||
match args.single_quoted::<String>() {
|
||||
Ok(s) => msg.reply(
|
||||
&ctx,
|
||||
MessageBuilder::new()
|
||||
.push("you asked ")
|
||||
.push_bold_safe(s)
|
||||
.push(format!(
|
||||
", so I rolled a 🎲 of **{}** faces, and got **{}**!",
|
||||
dice, result
|
||||
))
|
||||
.build(),
|
||||
),
|
||||
Err(_) if args.is_empty() => msg.reply(
|
||||
&ctx,
|
||||
format!(
|
||||
"I rolled a 🎲 of **{}** faces, and got **{}**!",
|
||||
dice, result
|
||||
),
|
||||
),
|
||||
Err(e) => return Err(e.into()),
|
||||
}?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[command]
|
||||
#[description = r#"👈 Pick a choice from the available list of choices.
|
||||
You may prefix the first choice with `?` to make it a question!
|
||||
If no choices are given, Youmu defaults to `Yes!` and `No!`"#]
|
||||
#[usage = "[?question]/[choice #1]/[choice #2]/..."]
|
||||
#[example = "?What for dinner/Pizza/Hamburger"]
|
||||
fn pick(ctx: &mut Context, msg: &Message, mut args: Args) -> CommandResult {
|
||||
let (question, choices) = {
|
||||
// Get a list of options.
|
||||
let mut choices = args
|
||||
.quoted()
|
||||
.trimmed()
|
||||
.iter::<String>()
|
||||
.map(|v| v.unwrap())
|
||||
.peekable();
|
||||
// If we have the first argument as question, use it.
|
||||
let question = match choices.peek() {
|
||||
Some(ref q) if q.starts_with("?") => Some(q.replacen("?", "", 1) + "?"),
|
||||
_ => None,
|
||||
};
|
||||
// If we have a question, that's not a choice.
|
||||
let mut choices = match question {
|
||||
Some(_) => {
|
||||
choices.next();
|
||||
choices
|
||||
}
|
||||
None => choices,
|
||||
};
|
||||
// If there are no choices, default to Yes! and No!
|
||||
let choices = match choices.peek() {
|
||||
None => vec!["Yes!".to_owned(), "No!".to_owned()],
|
||||
_ => choices.collect(),
|
||||
};
|
||||
(question, choices)
|
||||
};
|
||||
|
||||
let choice = {
|
||||
let uniform = Uniform::from(0..choices.len());
|
||||
let mut rng = thread_rng();
|
||||
&choices[uniform.sample(&mut rng)]
|
||||
};
|
||||
|
||||
match question {
|
||||
None => msg.reply(
|
||||
&ctx,
|
||||
MessageBuilder::new()
|
||||
.push("Youmu picks 👉")
|
||||
.push_bold_safe(choice)
|
||||
.push("👈!")
|
||||
.build(),
|
||||
),
|
||||
Some(s) => msg.reply(
|
||||
&ctx,
|
||||
MessageBuilder::new()
|
||||
.push("you asked ")
|
||||
.push_bold_safe(s)
|
||||
.push(", and Youmu picks 👉")
|
||||
.push_bold_safe(choice)
|
||||
.push("👈!")
|
||||
.build(),
|
||||
),
|
||||
}?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[command]
|
||||
#[description = "Wanna know what your name is in Japanese🇯🇵?"]
|
||||
#[usage = "[user_mention = yourself]"]
|
||||
#[example = "@user#1234"]
|
||||
#[max_args(1)]
|
||||
fn name(ctx: &mut Context, msg: &Message, mut args: Args) -> CommandResult {
|
||||
let user_id = if args.is_empty() {
|
||||
msg.author.id
|
||||
} else {
|
||||
args.single::<UserId>()?
|
||||
};
|
||||
|
||||
let user_mention = if user_id == msg.author.id {
|
||||
"your".to_owned()
|
||||
} else {
|
||||
MessageBuilder::new()
|
||||
.push_bold_safe(user_id.to_user(&ctx)?.tag())
|
||||
.push("'s")
|
||||
.build()
|
||||
};
|
||||
|
||||
// Rule out a couple of cases
|
||||
if user_id == ctx.http.get_current_application_info()?.id {
|
||||
// This is my own user_id
|
||||
msg.reply(&ctx, "😠 My name is **Youmu Konpaku**!")?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let (first_name, last_name) = names::name_from_userid(user_id);
|
||||
|
||||
msg.reply(
|
||||
&ctx,
|
||||
format!(
|
||||
"{} Japanese🇯🇵 name is **{} {}**!",
|
||||
user_mention, first_name, last_name
|
||||
),
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
1547
youmubot-core/src/fun/names.rs
Normal file
1547
youmubot-core/src/fun/names.rs
Normal file
File diff suppressed because it is too large
Load diff
44
youmubot-core/src/lib.rs
Normal file
44
youmubot-core/src/lib.rs
Normal file
|
@ -0,0 +1,44 @@
|
|||
use serenity::{
|
||||
framework::standard::{
|
||||
help_commands, macros::help, Args, CommandGroup, CommandResult, HelpOptions,
|
||||
},
|
||||
model::{channel::Message, id::UserId},
|
||||
};
|
||||
use std::collections::HashSet;
|
||||
use youmubot_prelude::*;
|
||||
|
||||
pub mod admin;
|
||||
pub mod community;
|
||||
mod db;
|
||||
pub mod fun;
|
||||
|
||||
pub use admin::ADMIN_GROUP;
|
||||
pub use community::COMMUNITY_GROUP;
|
||||
pub use fun::FUN_GROUP;
|
||||
|
||||
/// Sets up all databases in the client.
|
||||
pub fn setup(
|
||||
path: &std::path::Path,
|
||||
client: &serenity::client::Client,
|
||||
data: &mut youmubot_prelude::ShareMap,
|
||||
) -> serenity::framework::standard::CommandResult {
|
||||
db::SoftBans::insert_into(&mut *data, &path.join("soft_bans.yaml"))?;
|
||||
|
||||
// Create handler threads
|
||||
std::thread::spawn(admin::watch_soft_bans(client));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// A help command
|
||||
#[help]
|
||||
pub fn help(
|
||||
context: &mut Context,
|
||||
msg: &Message,
|
||||
args: Args,
|
||||
help_options: &'static HelpOptions,
|
||||
groups: &[&'static CommandGroup],
|
||||
owners: HashSet<UserId>,
|
||||
) -> CommandResult {
|
||||
help_commands::with_embeds(context, msg, args, help_options, groups, owners)
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue