mirror of
https://github.com/natsukagami/youmubot.git
synced 2025-04-19 16:58:55 +00:00
Add role messages
This commit is contained in:
parent
72c574fb48
commit
aa39b42e94
7 changed files with 368 additions and 8 deletions
2
Cargo.lock
generated
2
Cargo.lock
generated
|
@ -1848,6 +1848,8 @@ name = "youmubot-core"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"chrono",
|
"chrono",
|
||||||
|
"dashmap",
|
||||||
|
"flume",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"rand 0.7.3",
|
"rand 0.7.3",
|
||||||
"serde",
|
"serde",
|
||||||
|
|
|
@ -14,6 +14,8 @@ chrono = "0.4"
|
||||||
static_assertions = "1.1"
|
static_assertions = "1.1"
|
||||||
futures-util = "0.3"
|
futures-util = "0.3"
|
||||||
tokio = { version = "1", features = ["time"] }
|
tokio = { version = "1", features = ["time"] }
|
||||||
|
flume = "0.10"
|
||||||
|
dashmap = "4"
|
||||||
|
|
||||||
youmubot-db = { path = "../youmubot-db" }
|
youmubot-db = { path = "../youmubot-db" }
|
||||||
youmubot-prelude = { path = "../youmubot-prelude" }
|
youmubot-prelude = { path = "../youmubot-prelude" }
|
||||||
|
|
|
@ -19,13 +19,18 @@ use youmubot_prelude::*;
|
||||||
mod roles;
|
mod roles;
|
||||||
mod votes;
|
mod votes;
|
||||||
|
|
||||||
use roles::{ADD_COMMAND, LIST_COMMAND, REMOVE_COMMAND, TOGGLE_COMMAND};
|
use roles::{
|
||||||
|
ADD_COMMAND, LIST_COMMAND, REMOVE_COMMAND, RMROLEMESSAGE_COMMAND, ROLEMESSAGE_COMMAND,
|
||||||
|
TOGGLE_COMMAND,
|
||||||
|
};
|
||||||
use votes::VOTE_COMMAND;
|
use votes::VOTE_COMMAND;
|
||||||
|
|
||||||
|
pub use roles::ReactionWatchers;
|
||||||
|
|
||||||
#[group]
|
#[group]
|
||||||
#[description = "Community related commands. Usually comes with some sort of delays, since it involves pinging"]
|
#[description = "Community related commands. Usually comes with some sort of delays, since it involves pinging"]
|
||||||
#[only_in("guilds")]
|
#[only_in("guilds")]
|
||||||
#[commands(choose, vote, add, list, remove, toggle)]
|
#[commands(choose, vote, add, list, remove, toggle, rolemessage, rmrolemessage)]
|
||||||
struct Community;
|
struct Community;
|
||||||
|
|
||||||
#[command]
|
#[command]
|
||||||
|
|
|
@ -10,6 +10,8 @@ use serenity::{
|
||||||
};
|
};
|
||||||
use youmubot_prelude::*;
|
use youmubot_prelude::*;
|
||||||
|
|
||||||
|
pub use reaction_watcher::Watchers as ReactionWatchers;
|
||||||
|
|
||||||
#[command("listroles")]
|
#[command("listroles")]
|
||||||
#[description = "List all available roles in the server."]
|
#[description = "List all available roles in the server."]
|
||||||
#[num_args(0)]
|
#[num_args(0)]
|
||||||
|
@ -17,7 +19,6 @@ use youmubot_prelude::*;
|
||||||
async fn list(ctx: &Context, m: &Message, _: Args) -> CommandResult {
|
async fn list(ctx: &Context, m: &Message, _: Args) -> CommandResult {
|
||||||
let guild_id = m.guild_id.unwrap(); // only_in(guilds)
|
let guild_id = m.guild_id.unwrap(); // only_in(guilds)
|
||||||
let data = ctx.data.read().await;
|
let data = ctx.data.read().await;
|
||||||
|
|
||||||
let db = DB::open(&*data);
|
let db = DB::open(&*data);
|
||||||
let roles = db
|
let roles = db
|
||||||
.borrow()?
|
.borrow()?
|
||||||
|
@ -258,3 +259,330 @@ fn role_from_string(role: &str, roles: &std::collections::HashMap<RoleId, Role>)
|
||||||
.cloned(),
|
.cloned(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[command("rolemessage")]
|
||||||
|
#[description = "Create a message that handles roles in a list. All roles in the list must already be inside the set. Empty = all assignable roles."]
|
||||||
|
#[usage = "{title}/[role]/[role]/..."]
|
||||||
|
#[example = "Game Roles/Genshin/osu!"]
|
||||||
|
#[min_args(1)]
|
||||||
|
#[required_permissions(MANAGE_ROLES)]
|
||||||
|
#[only_in(guilds)]
|
||||||
|
async fn rolemessage(ctx: &Context, m: &Message, mut args: Args) -> CommandResult {
|
||||||
|
let title = args.single_quoted::<String>().unwrap();
|
||||||
|
let data = ctx.data.read().await;
|
||||||
|
let guild_id = m.guild_id.unwrap();
|
||||||
|
let assignables = DB::open(&*data)
|
||||||
|
.borrow()?
|
||||||
|
.get(&guild_id)
|
||||||
|
.filter(|v| !v.roles.is_empty())
|
||||||
|
.map(|r| r.roles.clone())
|
||||||
|
.unwrap_or_default();
|
||||||
|
let mut rolenames = args
|
||||||
|
.iter::<String>()
|
||||||
|
.quoted()
|
||||||
|
.collect::<Result<Vec<_>, _>>()
|
||||||
|
.unwrap();
|
||||||
|
let rolelist = guild_id.to_partial_guild(&ctx).await?.roles;
|
||||||
|
let mut roles = Vec::new();
|
||||||
|
if rolenames.is_empty() {
|
||||||
|
rolenames = assignables.keys().map(|v| v.to_string()).collect();
|
||||||
|
}
|
||||||
|
for rolename in rolenames {
|
||||||
|
let role = match role_from_string(rolename.as_str(), &rolelist) {
|
||||||
|
Some(role) => role,
|
||||||
|
None => {
|
||||||
|
m.reply(&ctx, format!("Role `{}` not found", rolename))
|
||||||
|
.await?;
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let role = match assignables.get(&role.id) {
|
||||||
|
Some(r) => match &r.reaction {
|
||||||
|
Some(emote) => (r.clone(), role.clone(), emote.clone()),
|
||||||
|
None => {
|
||||||
|
m.reply(
|
||||||
|
&ctx,
|
||||||
|
format!("Role `{}` does not have a assignable emote.", rolename),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
},
|
||||||
|
None => {
|
||||||
|
m.reply(&ctx, format!("Role `{}` is not assignable.", rolename))
|
||||||
|
.await?;
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
roles.push(role);
|
||||||
|
}
|
||||||
|
data.get::<ReactionWatchers>()
|
||||||
|
.unwrap()
|
||||||
|
.add(ctx.clone(), guild_id, m.channel_id, title, roles)
|
||||||
|
.await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[command("rmrolemessage")]
|
||||||
|
#[description = "Delete a role message handler."]
|
||||||
|
#[usage = "(reply to the message to delete)"]
|
||||||
|
#[num_args(0)]
|
||||||
|
#[required_permissions(MANAGE_ROLES)]
|
||||||
|
#[only_in(guilds)]
|
||||||
|
async fn rmrolemessage(ctx: &Context, m: &Message, _args: Args) -> CommandResult {
|
||||||
|
let data = ctx.data.read().await;
|
||||||
|
let guild_id = m.guild_id.unwrap();
|
||||||
|
|
||||||
|
let message = match &m.referenced_message {
|
||||||
|
Some(m) => m,
|
||||||
|
None => {
|
||||||
|
m.reply(&ctx, "No replied message found.").await?;
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if data
|
||||||
|
.get::<ReactionWatchers>()
|
||||||
|
.unwrap()
|
||||||
|
.remove(ctx, guild_id, message.id)
|
||||||
|
.await?
|
||||||
|
{
|
||||||
|
message.delete(&ctx).await.ok();
|
||||||
|
m.react(&ctx, '👌').await.ok();
|
||||||
|
} else {
|
||||||
|
m.reply(&ctx, "Message does not come with a reaction handler")
|
||||||
|
.await
|
||||||
|
.ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
mod reaction_watcher {
|
||||||
|
use crate::db::{Role, RoleMessage, Roles};
|
||||||
|
use dashmap::DashMap;
|
||||||
|
use flume::{Receiver, Sender};
|
||||||
|
use serenity::{
|
||||||
|
collector::ReactionAction,
|
||||||
|
model::{
|
||||||
|
channel::ReactionType,
|
||||||
|
guild::Role as DiscordRole,
|
||||||
|
id::{ChannelId, GuildId, MessageId},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
use youmubot_prelude::*;
|
||||||
|
|
||||||
|
/// A set of watchers.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Watchers {
|
||||||
|
watchers: DashMap<MessageId, Watcher>,
|
||||||
|
|
||||||
|
init: Mutex<Vec<(GuildId, MessageId, RoleMessage)>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Watchers {
|
||||||
|
pub fn new(data: &TypeMap) -> Result<Self> {
|
||||||
|
let init = Roles::open(&*data)
|
||||||
|
.borrow()?
|
||||||
|
.iter()
|
||||||
|
.flat_map(|(&guild, rs)| {
|
||||||
|
rs.reaction_messages
|
||||||
|
.iter()
|
||||||
|
.map(move |(m, r)| (guild, m.clone(), r.clone()))
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
Ok(Self {
|
||||||
|
init: Mutex::new(init),
|
||||||
|
watchers: DashMap::new(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
pub async fn init(&self, ctx: &Context) {
|
||||||
|
let mut init = self.init.lock().await;
|
||||||
|
for (msg, watcher) in init
|
||||||
|
.drain(..)
|
||||||
|
.map(|(guild, msg, rm)| (msg, Watcher::spawn(ctx.clone(), guild, rm.id)))
|
||||||
|
{
|
||||||
|
self.watchers.insert(msg, watcher);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub async fn add(
|
||||||
|
&self,
|
||||||
|
ctx: Context,
|
||||||
|
guild: GuildId,
|
||||||
|
channel: ChannelId,
|
||||||
|
title: String,
|
||||||
|
roles: Vec<(Role, DiscordRole, ReactionType)>,
|
||||||
|
) -> Result<()> {
|
||||||
|
// Send a message
|
||||||
|
let msg = channel
|
||||||
|
.send_message(&ctx, |m| {
|
||||||
|
m.content({
|
||||||
|
let mut builder = serenity::utils::MessageBuilder::new();
|
||||||
|
builder
|
||||||
|
.push_bold("Role Menu:")
|
||||||
|
.push(" ")
|
||||||
|
.push_bold_line_safe(&title)
|
||||||
|
.push_line("React to give yourself a role.")
|
||||||
|
.push_line("");
|
||||||
|
for (role, discord_role, emoji) in &roles {
|
||||||
|
builder
|
||||||
|
.push(emoji)
|
||||||
|
.push(" ")
|
||||||
|
.push_bold_safe(&discord_role.name)
|
||||||
|
.push(": ")
|
||||||
|
.push_line_safe(&role.description)
|
||||||
|
.push_line("");
|
||||||
|
}
|
||||||
|
builder
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
// Do reactions
|
||||||
|
roles
|
||||||
|
.iter()
|
||||||
|
.map(|(_, _, emoji)| {
|
||||||
|
msg.react(&ctx, emoji.clone()).map(|r| {
|
||||||
|
r.ok();
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect::<stream::FuturesUnordered<_>>()
|
||||||
|
.collect::<()>()
|
||||||
|
.await;
|
||||||
|
// Store the message into the list.
|
||||||
|
{
|
||||||
|
let data = ctx.data.read().await;
|
||||||
|
Roles::open(&*data)
|
||||||
|
.borrow_mut()?
|
||||||
|
.entry(guild)
|
||||||
|
.or_default()
|
||||||
|
.reaction_messages
|
||||||
|
.insert(
|
||||||
|
msg.id,
|
||||||
|
RoleMessage {
|
||||||
|
id: msg.id,
|
||||||
|
title,
|
||||||
|
roles: roles.into_iter().map(|(a, _, b)| (a, b)).collect(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// Spawn the handler
|
||||||
|
self.watchers
|
||||||
|
.insert(msg.id, Watcher::spawn(ctx, guild, msg.id));
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn remove(
|
||||||
|
&self,
|
||||||
|
ctx: &Context,
|
||||||
|
guild: GuildId,
|
||||||
|
message: MessageId,
|
||||||
|
) -> Result<bool> {
|
||||||
|
let data = ctx.data.read().await;
|
||||||
|
Roles::open(&*data)
|
||||||
|
.borrow_mut()?
|
||||||
|
.entry(guild)
|
||||||
|
.or_default()
|
||||||
|
.reaction_messages
|
||||||
|
.remove(&message);
|
||||||
|
Ok(self.watchers.remove(&message).is_some())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TypeMapKey for Watchers {
|
||||||
|
type Value = Watchers;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A reaction watcher structure. Contains a cancel signaler that cancels the watcher upon Drop.
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct Watcher {
|
||||||
|
cancel: Sender<()>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Watcher {
|
||||||
|
pub fn spawn(ctx: Context, guild: GuildId, message: MessageId) -> Self {
|
||||||
|
let (send, recv) = flume::bounded(0);
|
||||||
|
tokio::spawn(Self::handle(ctx, recv, guild, message));
|
||||||
|
Watcher { cancel: send }
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle(ctx: Context, recv: Receiver<()>, guild: GuildId, message: MessageId) {
|
||||||
|
let mut recv = recv.into_recv_async();
|
||||||
|
let collect = || {
|
||||||
|
serenity::collector::CollectReaction::new(&ctx)
|
||||||
|
.message_id(message)
|
||||||
|
.removed(true)
|
||||||
|
};
|
||||||
|
loop {
|
||||||
|
let reaction = match future::select(recv, collect()).await {
|
||||||
|
future::Either::Left(_) => break,
|
||||||
|
future::Either::Right((r, new_recv)) => {
|
||||||
|
recv = new_recv;
|
||||||
|
match r {
|
||||||
|
Some(r) => r,
|
||||||
|
None => continue,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
eprintln!("{:?}", reaction);
|
||||||
|
if let Err(e) = Self::handle_reaction(&ctx, guild, message, &*reaction).await {
|
||||||
|
eprintln!("Handling {:?}: {}", reaction, e);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle_reaction(
|
||||||
|
ctx: &Context,
|
||||||
|
guild: GuildId,
|
||||||
|
message: MessageId,
|
||||||
|
reaction: &ReactionAction,
|
||||||
|
) -> Result<()> {
|
||||||
|
let data = ctx.data.read().await;
|
||||||
|
// Collect user
|
||||||
|
let user_id = match reaction.as_inner_ref().user_id {
|
||||||
|
Some(id) => id,
|
||||||
|
None => return Ok(()),
|
||||||
|
};
|
||||||
|
let mut member = match guild.member(ctx, user_id).await.ok() {
|
||||||
|
Some(m) => m,
|
||||||
|
None => return Ok(()),
|
||||||
|
};
|
||||||
|
if member.user.bot {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
// Get the role list.
|
||||||
|
let role = Roles::open(&*data)
|
||||||
|
.borrow()?
|
||||||
|
.get(&guild)
|
||||||
|
.ok_or(Error::msg("guild no longer has role list"))?
|
||||||
|
.reaction_messages
|
||||||
|
.get(&message)
|
||||||
|
.map(|msg| &msg.roles[..])
|
||||||
|
.ok_or(Error::msg("message is no longer a role list handler"))?
|
||||||
|
.iter()
|
||||||
|
.find_map(|(role, role_reaction)| {
|
||||||
|
if &reaction.as_inner_ref().emoji == role_reaction {
|
||||||
|
Some(role.id)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
});
|
||||||
|
let role = match role {
|
||||||
|
Some(id) => id,
|
||||||
|
None => return Ok(()),
|
||||||
|
};
|
||||||
|
|
||||||
|
match reaction {
|
||||||
|
ReactionAction::Added(_) => member.add_role(&ctx, role).await.pls_ok(),
|
||||||
|
ReactionAction::Removed(_) => member.remove_role(&ctx, role).await.pls_ok(),
|
||||||
|
};
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for Watcher {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
self.cancel.send(()).unwrap()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,7 +1,10 @@
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use serenity::model::id::{RoleId, UserId};
|
use serenity::model::{
|
||||||
|
channel::ReactionType,
|
||||||
|
id::{MessageId, RoleId, UserId},
|
||||||
|
};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use youmubot_db::{GuildMap, DB};
|
use youmubot_db::{GuildMap, DB};
|
||||||
use youmubot_prelude::*;
|
use youmubot_prelude::*;
|
||||||
|
@ -38,7 +41,7 @@ impl ServerSoftBans {
|
||||||
#[derive(Serialize, Deserialize, Debug, Clone, Default)]
|
#[derive(Serialize, Deserialize, Debug, Clone, Default)]
|
||||||
pub struct RoleList {
|
pub struct RoleList {
|
||||||
/// `reaction_message` handles the reaction-handling message.
|
/// `reaction_message` handles the reaction-handling message.
|
||||||
pub reaction_message: Option<serenity::model::id::MessageId>,
|
pub reaction_messages: HashMap<MessageId, RoleMessage>,
|
||||||
pub roles: HashMap<RoleId, Role>,
|
pub roles: HashMap<RoleId, Role>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -68,7 +71,7 @@ pub fn load_role_list(
|
||||||
(
|
(
|
||||||
guild,
|
guild,
|
||||||
RoleList {
|
RoleList {
|
||||||
reaction_message: None,
|
reaction_messages: HashMap::new(),
|
||||||
roles,
|
roles,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
@ -85,13 +88,21 @@ pub fn load_role_list(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A single role in the list of role messages.
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||||
|
pub struct RoleMessage {
|
||||||
|
pub id: serenity::model::id::MessageId,
|
||||||
|
pub title: String,
|
||||||
|
pub roles: Vec<(Role, ReactionType)>,
|
||||||
|
}
|
||||||
|
|
||||||
/// Role represents an assignable role.
|
/// Role represents an assignable role.
|
||||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||||
pub struct Role {
|
pub struct Role {
|
||||||
pub id: RoleId,
|
pub id: RoleId,
|
||||||
pub description: String,
|
pub description: String,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub reaction: Option<serenity::model::channel::ReactionType>,
|
pub reaction: Option<ReactionType>,
|
||||||
}
|
}
|
||||||
|
|
||||||
mod legacy {
|
mod legacy {
|
||||||
|
|
|
@ -35,6 +35,9 @@ pub fn setup(
|
||||||
client.data.clone(),
|
client.data.clone(),
|
||||||
));
|
));
|
||||||
|
|
||||||
|
// Start reaction handlers
|
||||||
|
data.insert::<community::ReactionWatchers>(community::ReactionWatchers::new(&*data)?);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -27,7 +27,16 @@ impl Handler {
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl EventHandler for Handler {
|
impl EventHandler for Handler {
|
||||||
async fn ready(&self, _: Context, ready: gateway::Ready) {
|
async fn ready(&self, ctx: Context, ready: gateway::Ready) {
|
||||||
|
// Start ReactionWatchers for community.
|
||||||
|
#[cfg(feature = "core")]
|
||||||
|
ctx.data
|
||||||
|
.read()
|
||||||
|
.await
|
||||||
|
.get::<youmubot_core::community::ReactionWatchers>()
|
||||||
|
.unwrap()
|
||||||
|
.init(&ctx)
|
||||||
|
.await;
|
||||||
println!("{} is connected!", ready.user.name);
|
println!("{} is connected!", ready.user.name);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue