Unverified Commit 5db256a7 authored by tec's avatar tec

Breaking serenity upgrade: start changing to async

parent 10309cbb
Pipeline #290 failed with stages
in 6 minutes and 37 seconds
This diff is collapsed.
......@@ -5,6 +5,7 @@ authors = ["tec <[email protected]>"]
edition = "2018"
[dependencies]
async-trait = "0.1.42"
base64 = "0.13.0"
chrono = "0.4.19"
diesel = { version = "1.4.5", features = ["sqlite"] }
......@@ -23,4 +24,5 @@ serde = "1.0.118"
serde_yaml = "0.8.15"
serenity = "0.10.1"
simplelog = "0.9.0"
tokio = { version = "1", features = ["full"] }
url = "2.2.0"
......@@ -18,7 +18,8 @@ pub struct UccbotConfig {
pub welcome_channel: id::ChannelId,
pub announcement_channel: id::ChannelId,
pub readme_channel: id::ChannelId,
pub bot_id: u64,
pub bot_id: id::UserId,
pub ical_url: Option<String>,
pub vote_pool_size: i8,
pub vote_role: u64,
pub tiebreaker_role: u64,
......
......@@ -14,6 +14,8 @@ extern crate ldap3;
extern crate reqwest;
extern crate tokio;
use simplelog::*;
use std::fs::File;
......@@ -34,9 +36,10 @@ mod voting;
use config::SECRETS;
use serenity_handler::Handler;
fn main() {
#[tokio::main]
async fn main() {
CombinedLogger::init(vec![
TermLogger::new(LevelFilter::Info, Config::default(), TerminalMode::Mixed).unwrap(),
TermLogger::new(LevelFilter::Info, Config::default(), TerminalMode::Mixed),
WriteLogger::new(
LevelFilter::Info,
Config::default(),
......@@ -45,16 +48,21 @@ fn main() {
])
.unwrap();
// ical::fetch_latest_ical().wait();
// Create a new instance of the Client, logging in as a bot. This will
// automatically prepend your bot token with "Bot ", which is a requirement
// by Discord for bot users.
let mut client = Client::new(&SECRETS.discord_token, Handler).expect("Err creating client");
let mut client = Client::builder(&SECRETS.discord_token)
.event_handler(Handler)
.await
.expect("Err creating client");
// Finally, start a single shard, and start listening to events.
//
// Shards will automatically attempt to reconnect, and will perform
// exponential backoff until it reconnects.
if let Err(why) = client.start() {
if let Err(why) = client.start().await {
error!("Client error: {:?}", why);
}
}
......@@ -8,10 +8,12 @@ use serenity::{
use std::collections::{HashMap, HashSet};
use std::iter::FromIterator;
pub fn add_role_by_reaction(ctx: &Context, msg: Message, added_reaction: Reaction) {
pub async fn add_role_by_reaction(ctx: &Context, msg: Message, added_reaction: Reaction) {
let user = added_reaction
.user_id
.unwrap()
.to_user(ctx)
.await
.expect("Unable to get user");
if let Some(role_id) = CONFIG
.react_role_messages
......@@ -27,24 +29,29 @@ pub fn add_role_by_reaction(ctx: &Context, msg: Message, added_reaction: Reactio
user.name,
role_id
.to_role_cached(ctx)
.await
.expect("Unable to get role")
.name
);
ctx.http
.add_member_role(
CONFIG.server_id,
added_reaction.user_id.0,
*added_reaction.user_id.unwrap().as_u64(),
*role_id.as_u64(),
)
.await
.ok();
} else {
warn!("{} provided invalid react for role", user.name);
e!("Unable to delete react: {:?}", added_reaction.delete(ctx));
e!(
"Unable to delete react: {:?}",
added_reaction.delete(ctx).await
);
}
}
pub fn remove_role_by_reaction(ctx: &Context, msg: Message, removed_reaction: Reaction) {
CONFIG
pub async fn remove_role_by_reaction(ctx: &Context, msg: Message, removed_reaction: Reaction) {
let role_id = CONFIG
.react_role_messages
.iter()
.find(|rrm| rrm.message == msg.id)
......@@ -52,30 +59,31 @@ pub fn remove_role_by_reaction(ctx: &Context, msg: Message, removed_reaction: Re
let react_as_string = get_string_from_react(&removed_reaction.emoji);
reaction_mapping.mapping.get(&react_as_string)
})
.and_then(|role_id| {
info!(
"{} requested removal of role '{}'",
msg.author.name,
role_id
.to_role_cached(ctx)
.expect("Unable to get role")
.name
);
ctx.http
.remove_member_role(
CONFIG.server_id,
removed_reaction.user_id.0,
*role_id.as_u64(),
)
.ok()
});
.unwrap();
info!(
"{} requested removal of role '{}'",
msg.author.name,
role_id
.to_role_cached(ctx)
.await
.expect("Unable to get role")
.name
);
ctx.http
.remove_member_role(
CONFIG.server_id,
*removed_reaction.user_id.unwrap().as_u64(),
*role_id.as_u64(),
)
.await
.ok();
}
pub fn sync_all_role_reactions(ctx: &Context) {
pub async fn sync_all_role_reactions(ctx: &Context) {
info!("Syncing roles to reactions");
let messages_with_role_mappings = get_all_role_reaction_message(ctx);
info!(" Sync: reaction messages fetched");
let guild = ctx.http.get_guild(CONFIG.server_id).unwrap();
let guild = ctx.http.get_guild(CONFIG.server_id).await.unwrap();
info!(" Sync: guild fetched");
// this method supports paging, but we probably don't need it since the server only has a couple of
// hundred members. the Reaction.users() method can apparently only retrieve 100 users at once, but
......@@ -83,16 +91,17 @@ pub fn sync_all_role_reactions(ctx: &Context) {
let mut all_members = ctx
.http
.get_guild_members(CONFIG.server_id, Some(1000), None)
.await
.unwrap();
all_members.retain(|m| m.user_id() != CONFIG.bot_id);
all_members.retain(|m| m.user.id != CONFIG.bot_id);
info!(" Sync: all members fetched");
let mut roles_to_add: HashMap<UserId, Vec<RoleId>> =
HashMap::from_iter(all_members.iter().map(|m| (m.user_id(), Vec::new())));
HashMap::from_iter(all_members.iter().map(|m| (m.user.id, Vec::new())));
let mut roles_to_remove: HashMap<UserId, Vec<RoleId>> =
HashMap::from_iter(all_members.iter().map(|m| (m.user_id(), Vec::new())));
HashMap::from_iter(all_members.iter().map(|m| (m.user.id, Vec::new())));
for (i, (message, mapping)) in messages_with_role_mappings.iter().enumerate() {
for (i, (message, mapping)) in messages_with_role_mappings.await.iter().enumerate() {
info!(" Sync: prossessing message #{}", i);
for react in &message.reactions {
let react_as_string = get_string_from_react(&react.reaction_type);
......@@ -105,6 +114,7 @@ pub fn sync_all_role_reactions(ctx: &Context) {
);
for illegal_react_user in &message
.reaction_users(&ctx.http, react.reaction_type.clone(), Some(100), None)
.await
.unwrap_or(vec![])
{
message
......@@ -115,6 +125,7 @@ pub fn sync_all_role_reactions(ctx: &Context) {
Some(illegal_react_user.id),
react.reaction_type.clone(),
)
.await
.expect("Unable to delete react");
}
}
......@@ -124,6 +135,7 @@ pub fn sync_all_role_reactions(ctx: &Context) {
let reaction_type = get_react_from_string(react.clone(), guild.clone());
let reactors = message
.reaction_users(ctx.http.clone(), reaction_type.clone(), Some(100), None)
.await
.unwrap();
let reactor_ids: HashSet<UserId> = HashSet::from_iter(reactors.iter().map(|r| r.id));
......@@ -131,12 +143,12 @@ pub fn sync_all_role_reactions(ctx: &Context) {
if !reactor_ids.contains(&UserId::from(CONFIG.bot_id)) {
e!(
"Unable to add reaction, {:?}",
message.react(ctx, reaction_type)
message.react(ctx, reaction_type).await
);
}
for member in all_members.clone() {
let user_id = &member.user_id();
let user_id = &member.user.id;
if reactor_ids.contains(&user_id) {
if !member.roles.iter().any(|r| r == role) {
roles_to_add.get_mut(&user_id).unwrap().push(*role);
......@@ -153,10 +165,13 @@ pub fn sync_all_role_reactions(ctx: &Context) {
if !roles.is_empty() {
let mut member = all_members
.iter()
.find(|m| m.user_id() == user_id)
.find(|m| m.user.id == user_id)
.unwrap()
.clone();
member.add_roles(ctx.http.clone(), &roles[..]).unwrap();
member
.add_roles(ctx.http.clone(), &roles[..])
.await
.unwrap();
}
}
info!(" Sync: (any) missing roles added");
......@@ -164,20 +179,23 @@ pub fn sync_all_role_reactions(ctx: &Context) {
if !roles.is_empty() {
let mut member = all_members
.iter()
.find(|m| m.user_id() == user_id)
.find(|m| m.user.id == user_id)
.unwrap()
.clone();
member.remove_roles(ctx.http.clone(), &roles[..]).unwrap();
member
.remove_roles(ctx.http.clone(), &roles[..])
.await
.unwrap();
}
}
info!(" Sync: (any) superflous roles removed");
info!("Role reaction sync complete");
}
fn get_all_role_reaction_message(ctx: &Context) -> Vec<(Message, &'static ReactRoleMap)> {
let guild = ctx.http.get_guild(CONFIG.server_id).unwrap();
async fn get_all_role_reaction_message(ctx: &Context) -> Vec<(Message, &'static ReactRoleMap)> {
let guild = ctx.http.get_guild(CONFIG.server_id).await.unwrap();
info!(" Find role-react message: guild determined");
let channels = ctx.http.get_channels(*guild.id.as_u64()).unwrap();
let channels = ctx.http.get_channels(*guild.id.as_u64()).await.unwrap();
info!(" Find role-react message: channels determined");
let http = ctx.http.clone();
channels
......@@ -189,8 +207,9 @@ fn get_all_role_reaction_message(ctx: &Context) -> Vec<(Message, &'static ReactR
CONFIG
.react_role_messages
.par_iter()
.filter_map(move |rrm| {
.filter_map(move |rrm| async {
h.get_message(*channel.id.as_u64(), *rrm.message.as_u64())
.await
.ok()
.map(|m| (m, &rrm.mapping))
})
......
use async_trait::async_trait;
use chrono::prelude::Utc;
use serenity::{
model::{channel, channel::Message, gateway::Ready, guild::Member},
......@@ -18,13 +19,14 @@ use crate::voting;
pub struct Handler;
#[async_trait]
impl EventHandler for Handler {
// Set a handler for the `message` event - so that whenever a new message
// is received - the closure (or function) passed will be called.
//
// Event handlers are dispatched through a threadpool, and so multiple
// events can be dispatched simultaneously.
fn message(&self, ctx: Context, msg: Message) {
async fn message(&self, ctx: Context, msg: Message) {
if !(msg.content.starts_with(&CONFIG.command_prefix)) {
if msg.content.contains(&format!("<@!{}>", CONFIG.bot_id)) // desktop mentions
|| msg.content.contains(&format!("<@{}>", CONFIG.bot_id))
......@@ -48,15 +50,15 @@ impl EventHandler for Handler {
};
match message_content[0] {
"say" => println!("{:#?}", msg.content),
"register" => user_management::Commands::register(ctx, msg.clone(), content),
"verify" => user_management::Commands::verify(ctx, msg.clone(), content),
"profile" => user_management::Commands::profile(ctx, msg.clone(), content),
"set" => user_management::Commands::set_info(ctx, msg.clone(), content),
"clear" => user_management::Commands::clear_info(ctx, msg.clone(), content),
"move" => voting::Commands::move_something(ctx, msg.clone(), content),
"motion" => voting::Commands::motion(ctx, msg.clone(), content),
"poll" => voting::Commands::poll(ctx, msg.clone(), content),
"cowsay" => voting::Commands::cowsay(ctx, msg.clone(), content),
"register" => user_management::Commands::register(ctx, msg.clone(), content).await,
"verify" => user_management::Commands::verify(ctx, msg.clone(), content).await,
"profile" => user_management::Commands::profile(ctx, msg.clone(), content).await,
"set" => user_management::Commands::set_info(ctx, msg.clone(), content).await,
"clear" => user_management::Commands::clear_info(ctx, msg.clone(), content).await,
"move" => voting::Commands::move_something(ctx, msg.clone(), content).await,
"motion" => voting::Commands::motion(ctx, msg.clone(), content).await,
"poll" => voting::Commands::poll(ctx, msg.clone(), content).await,
"cowsay" => voting::Commands::cowsay(ctx, msg.clone(), content).await,
"source" => {
let mut mesg = MessageBuilder::new();
mesg.push(
......@@ -96,13 +98,16 @@ impl EventHandler for Handler {
});
m
});
if let Err(why) = result {
if let Err(why) = result.await {
error!("Error sending help embed: {:?}", why);
}
}
// undocumented (in !help) functins
"logreact" => {
e!("Error deleting logreact prompt: {:?}", msg.delete(&ctx));
e!(
"Error deleting logreact prompt: {:?}",
msg.delete(&ctx).await
);
send_message!(
msg.channel_id,
&ctx.http,
......@@ -119,7 +124,7 @@ impl EventHandler for Handler {
&ctx.http,
format!("{:?}", ldap::tla_search(message_content[1]))
),
"updateroles" => user_management::Commands::update_registered_role(ctx, msg),
"updateroles" => user_management::Commands::update_registered_role(ctx, msg).await,
_ => send_message!(
msg.channel_id,
&ctx.http,
......@@ -128,21 +133,21 @@ impl EventHandler for Handler {
}
}
fn reaction_add(&self, ctx: Context, add_reaction: channel::Reaction) {
match add_reaction.message(&ctx.http) {
async fn reaction_add(&self, ctx: Context, add_reaction: channel::Reaction) {
match add_reaction.message(&ctx.http).await {
Ok(message) => match get_message_type(&message) {
MessageType::RoleReactMessage if add_reaction.user_id.0 != CONFIG.bot_id => {
MessageType::RoleReactMessage if add_reaction.user_id.unwrap() != CONFIG.bot_id => {
add_role_by_reaction(&ctx, message, add_reaction);
return;
}
_ if message.author.id.0 != CONFIG.bot_id
|| add_reaction.user_id == CONFIG.bot_id =>
_ if message.author.id != CONFIG.bot_id
|| add_reaction.user_id.unwrap() == CONFIG.bot_id =>
{
return
}
MessageType::Motion => voting::reaction_add(ctx, add_reaction),
MessageType::Motion => voting::reaction_add(ctx, add_reaction).await,
MessageType::LogReact => {
let react_user = add_reaction.user(&ctx).unwrap();
let react_user = add_reaction.user(&ctx).await.unwrap();
let react_as_string = get_string_from_react(&add_reaction.emoji);
if Utc::now().timestamp() - message.timestamp.timestamp() > 300 {
warn!(
......@@ -162,6 +167,11 @@ impl EventHandler for Handler {
add_reaction.emoji,
));
msg.push_mono(react_as_string);
message
.channel_id
.say(&ctx.http, msg.build())
.await
.unwrap();
send_message!(message.channel_id, &ctx.http, msg.build());
}
_ => {}
......@@ -170,19 +180,21 @@ impl EventHandler for Handler {
}
}
fn reaction_remove(&self, ctx: Context, removed_reaction: channel::Reaction) {
match removed_reaction.message(&ctx.http) {
async fn reaction_remove(&self, ctx: Context, removed_reaction: channel::Reaction) {
match removed_reaction.message(&ctx.http).await {
Ok(message) => match get_message_type(&message) {
MessageType::RoleReactMessage if removed_reaction.user_id != CONFIG.bot_id => {
MessageType::RoleReactMessage
if removed_reaction.user_id.unwrap() != CONFIG.bot_id =>
{
remove_role_by_reaction(&ctx, message, removed_reaction);
return;
}
_ if message.author.id.0 != CONFIG.bot_id
|| removed_reaction.user_id == CONFIG.bot_id =>
_ if message.author.id != CONFIG.bot_id
|| removed_reaction.user_id.unwrap() == CONFIG.bot_id =>
{
return
}
MessageType::Motion => voting::reaction_remove(ctx, removed_reaction),
MessageType::Motion => voting::reaction_remove(ctx, removed_reaction).await,
_ => {}
},
Err(why) => error!("Failed to get react message {:?}", why),
......
use async_trait::async_trait;
use rand::seq::SliceRandom;
use regex::Regex;
use serenity::{
......@@ -13,7 +14,7 @@ use crate::database;
use crate::ldap::{ldap_exists, ldap_search};
use crate::token_management::*;
pub fn new_member(ctx: &Context, mut new_member: Member) {
pub async fn new_member(ctx: &Context, mut new_member: Member) {
// TODO see if it's an old (registered) user re-joining, and act accordingly
let mut message = MessageBuilder::new();
message.push("Nice to see you here ");
......@@ -33,7 +34,10 @@ pub fn new_member(ctx: &Context, mut new_member: Member) {
message.mention(&CONFIG.welcome_channel);
send_message!(CONFIG.main_channel, &ctx, message.build());
if let Err(why) = new_member.add_role(&ctx.http, CONFIG.unregistered_member_role) {
if let Err(why) = new_member
.add_role(&ctx.http, CONFIG.unregistered_member_role)
.await
{
error!("Error adding user role: {:?}", why);
}
}
......@@ -72,7 +76,7 @@ pub const RESERVED_NAMES: &[&str] = &[
pub struct Commands;
impl Commands {
pub fn register(ctx: Context, msg: Message, account_name: &str) {
pub async fn register(ctx: Context, msg: Message, account_name: &str) {
if account_name.is_empty() {
send_message!(
msg.channel_id,
......@@ -111,7 +115,10 @@ impl Commands {
)
);
e!("Error deleting register message: {:?}", msg.delete(ctx));
e!(
"Error deleting register message: {:?}",
msg.delete(ctx).await
);
let message = Command::new("echo").arg(format!("<h3>Link your Discord account</h3>\
<p>Hi {}, to complete the link, go to the discord server and enter\
......@@ -138,17 +145,16 @@ impl Commands {
guard!(let Some(result) = ldap_search(&name) else {
return None
});
if result.login_shell.contains("locked")
&& CONFIG.expired_member_role > 0 {
return Some(CONFIG.expired_member_role)
if result.login_shell.contains("locked") && CONFIG.expired_member_role > 0 {
return Some(CONFIG.expired_member_role);
}
Some(CONFIG.registered_member_role)
}
// TODO: make this return a result
// NOTE: don't make this directly send messages, so it can be used for mass updates
pub fn update_registered_role(ctx: Context, msg: Message) {
guard!(let Ok(member_info) = database::get_member_info(&msg.author.id.0) else {
pub async fn update_registered_role(ctx: Context, msg: Message) {
guard!(let Ok(member_info) = database::get_member_info(msg.author.id.as_u64()) else {
return // Err()
});
guard!(let Some(registered_role) = Commands::get_registered_role(member_info.username) else {
......@@ -162,34 +168,40 @@ impl Commands {
let roles_to_remove = vec![
CONFIG.registered_member_role,
CONFIG.unregistered_member_role,
CONFIG.expired_member_role];
CONFIG.expired_member_role,
];
for role in roles_to_remove {
if role == registered_role { // remove when vec.remove_item is stable
continue
if role == registered_role {
// remove when vec.remove_item is stable
continue;
}
if discord_member.roles.contains(&RoleId::from(role))
&& discord_member.remove_role(&ctx.http, role).is_err() {
return // Err()
&& discord_member.remove_role(&ctx.http, role).is_err()
{
return; // Err()
}
}
if !discord_member.roles.contains(&RoleId::from(registered_role))
&& discord_member.add_role(&ctx.http, registered_role).is_err() {
return // Err()
if !discord_member
.roles
.contains(&RoleId::from(registered_role))
&& discord_member.add_role(&ctx.http, registered_role).is_err()
{
return; // Err()
}
// Ok()
}
pub fn verify(ctx: Context, msg: Message, token: &str) {
pub async fn verify(ctx: Context, msg: Message, token: &str) {
match parse_token(&msg.author, token) {
Ok(name) => {
e!(
"Unable to get member: {:?}",
serenity::model::id::GuildId(CONFIG.server_id)
.member(ctx.http.clone(), msg.author.id)
.map(|mut member| {
.map(|mut member| async {
let full_member = database::add_member(&msg.author.id.0, &name);
e!(
"Unable to remove role: {:?}",
......@@ -227,9 +239,12 @@ impl Commands {
format!("Verification error: {:?}", reason)
),
}
e!("Error deleting register message: {:?}", msg.delete(&ctx));
e!(
"Error deleting register message: {:?}",
msg.delete(&ctx).await
);
}
pub fn profile(ctx: Context, msg: Message, name: &str) {
pub async fn profile(ctx: Context, msg: Message, name: &str) {
let possible_member: Option<database::Member> = match if name.trim().is_empty() {
info!(
"{} (discord name) wants to look at their own profile",
......@@ -270,10 +285,11 @@ impl Commands {
let result = msg.channel_id.send_message(&ctx.http, |m| {
m.embed(|embed| {
embed.colour(serenity::utils::Colour::LIGHTER_GREY);
embed.footer(|f| {
embed.footer(|f| async {
let user = &ctx
.http
.get_user(member.discord_id.clone() as u64)
.await
.expect("We expected this user to exist... they didn't ;(");
f.text(&user.name);
f.icon_url(
......@@ -308,11 +324,11 @@ impl Commands {
});
m
});
if let Err(why) = result {
if let Err(why) = result.await {
error!("Error sending profile embed: {:?}", why);
}
}
pub fn set_info(ctx: Context, msg: Message, info: &str) {
pub async fn set_info(ctx: Context, msg: Message, info: &str) {
if info.trim().is_empty() {
msg.channel_id
.send_message(&ctx.http, |m| {
......@@ -334,7 +350,7 @@ impl Commands {
embed
});
m
})
}).await
.expect("Failed to send usage help embed");
return;
}
......@@ -379,6 +395,7 @@ impl Commands {
});
m
})
.await
.expect("Failed to send usage embed");
return;
}
......@@ -401,7 +418,7 @@ impl Commands {
}
}
}
guard!(let Ok(member) = database::get_member_info(&msg.author.id.0) else {
guard!(let Ok(member) = database::get_member_info(msg.author.id.as_u64()) else {
send_message!(
msg.channel_id,
&ctx.http,
......@@ -425,6 +442,7 @@ impl Commands {
info!(
"Set {}'s {} in profile to {}",
&msg.author_nick(ctx.http.clone())
.await
.unwrap_or(String::from("?")),
property,
value
......@@ -460,11 +478,11 @@ impl Commands {