Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
No results found
Show changes
Commits on Source (30)
/target/ /target/
Cargo.lock
**/*.rs.bk **/*.rs.bk
src/discord_token src/discord_token
ucc-bot.log ucc-bot.log
This diff is collapsed.
...@@ -5,20 +5,25 @@ authors = ["tec <tec@ucc.gu.uwa.edu.au>"] ...@@ -5,20 +5,25 @@ authors = ["tec <tec@ucc.gu.uwa.edu.au>"]
edition = "2018" edition = "2018"
[dependencies] [dependencies]
base64 = "^0.11" aes = "0.6"
chrono = "^0.4.10" async-trait = "0.1.42"
lazy_static = "^1.4.0" base64 = "0.13.0"
log = "^0.4.8" block-modes = "0.7"
openssl = "^0.10" chrono = "0.4.19"
rand = "^0.7.2" diesel = { version = "1.4.5", features = ["sqlite"] }
serde = "^1.0.104"
serde_yaml = "^0.8"
serenity = "0.8.0"
simplelog = "^0.7.4"
guard = "0.5.0" guard = "0.5.0"
indexmap = { version = "1.3.1", features = ["serde-1"] } ical = "0.7.0"
rayon = "1.3.0" indexmap = { version = "1.6.1", features = ["serde-1"] }
diesel = { version = "1.4.3", features = ["sqlite"] } lazy_static = "1.4.0"
ldap3 = "0.6" ldap3 = "0.9.1"
url = "^2.1" log = "0.4.11"
regex = "^1.3" rand = "0.8.1"
rayon = "1.5.0"
regex = "1.4.3"
reqwest = "0.11.0"
serde = "1.0.118"
serde_yaml = "0.8.15"
serenity = { version = "0.10.1", default-features = false, features = ["builder", "cache", "client", "gateway", "model", "http", "utils", "rustls_backend"]}
simplelog = "0.9.0"
tokio = { version = "1", features = ["full"] }
url = "2.2.0"
...@@ -18,12 +18,14 @@ pub struct UccbotConfig { ...@@ -18,12 +18,14 @@ pub struct UccbotConfig {
pub welcome_channel: id::ChannelId, pub welcome_channel: id::ChannelId,
pub announcement_channel: id::ChannelId, pub announcement_channel: id::ChannelId,
pub readme_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_pool_size: i8,
pub vote_role: u64, pub vote_role: u64,
pub tiebreaker_role: u64, pub tiebreaker_role: u64,
pub unregistered_member_role: u64, pub unregistered_member_role: u64,
pub registered_member_role: u64, pub registered_member_role: u64,
pub expired_member_role: u64,
pub command_prefix: String, pub command_prefix: String,
pub for_vote: String, pub for_vote: String,
pub against_vote: String, pub against_vote: String,
......
...@@ -2,6 +2,7 @@ server_id: 606351521117896704 # general ...@@ -2,6 +2,7 @@ server_id: 606351521117896704 # general
main_channel: 606351521117896706 # the-corner main_channel: 606351521117896706 # the-corner
welcome_channel: 606351613816209418 # general welcome_channel: 606351613816209418 # general
announcement_channel: 606351521117896706 # the-corner announcement_channel: 606351521117896706 # the-corner
readme_channel: 606351613816209418 # general
bot_id: 607078903969742848 bot_id: 607078903969742848
...@@ -10,6 +11,7 @@ vote_role: 607478818038480937 # Vote Role ...@@ -10,6 +11,7 @@ vote_role: 607478818038480937 # Vote Role
tiebreaker_role: 607509283483025409 # tie-breaker tiebreaker_role: 607509283483025409 # tie-breaker
unregistered_member_role: 608282247350714408 # unregistered unregistered_member_role: 608282247350714408 # unregistered
registered_member_role: 608282133118582815 # registered registered_member_role: 608282133118582815 # registered
expired_member_role: 607479030370926613 # registered
command_prefix: "!" command_prefix: "!"
for_vote: "👍" for_vote: "👍"
......
...@@ -5,12 +5,14 @@ announcement_channel: 264411219627212801 # committee ...@@ -5,12 +5,14 @@ announcement_channel: 264411219627212801 # committee
readme_channel: 674252245008908298 # readme readme_channel: 674252245008908298 # readme
bot_id: 635407267881156618 bot_id: 635407267881156618
ical_url: "https://calendar.google.com/calendar/ical/rb44is9l4dftsnk6lmf1qske6g%40group.calendar.google.com/public/basic.ics"
vote_pool_size: 8 # 4 exec + Fresher rep + 3 ocm vote_pool_size: 8 # 4 exec + Fresher rep + 3 ocm
vote_role: 269817189966544896 # @committee vote_role: 269817189966544896 # @committee
tiebreaker_role: 0 # No tiebreak apparently 635370432568098817 # @Presiding Presidenterino tiebreaker_role: 0 # No tiebreak apparently 635370432568098817 # @Presiding Presidenterino
unregistered_member_role: 674641042464833548 # @unregistered unregistered_member_role: 674641042464833548 # @unregistered
registered_member_role: 692754285557055490 # @member registered_member_role: 692754285557055490 # @member
expired_member_role: 692754285557055490 # @member
command_prefix: "!" command_prefix: "!"
...@@ -29,6 +31,19 @@ react_role_messages: ...@@ -29,6 +31,19 @@ react_role_messages:
🥤: 691852097003585577 🥤: 691852097003585577
cpu: 674255083063738368 cpu: 674255083063738368
🎮: 691852441091833896 🎮: 691852441091833896
🤖: 696241187035676763
📐: 696241431550885888
🧬: 696241817397624862
: 696241928492286044
🧮: 696242272844382209
💸: 696242320609378345
📚: 696257213068607518
🎨: 696242397407084614
: 696242439962230854
🌥️: 696242479527100506
: 696242521738838016
🌤: 696242572288589864
: 696242617301860403
- message: 674653030817464370 # Operating System - message: 674653030817464370 # Operating System
mapping: mapping:
win10: 674255085307691039 win10: 674255085307691039
...@@ -47,6 +62,7 @@ react_role_messages: ...@@ -47,6 +62,7 @@ react_role_messages:
atom_: 674255748426891274 atom_: 674255748426891274
intellij: 674255748959567901 intellij: 674255748959567901
npp: 674255750297812993 npp: 674255750297812993
eclipse: 696281046924394526
- message: 674653069526695946 # Programming (1) - message: 674653069526695946 # Programming (1)
mapping: mapping:
html: 674255751031685130 html: 674255751031685130
...@@ -74,3 +90,8 @@ react_role_messages: ...@@ -74,3 +90,8 @@ react_role_messages:
go: 674256091441397761 go: 674256091441397761
tex: 674256092187983897 tex: 674256092187983897
git: 674256093064593439 git: 674256093064593439
- message: 674653104666312704 # games
mapping:
et: 728997559061708922
factorio: 728997562710884373
minecraft: 728997565496033320
...@@ -13,6 +13,7 @@ vote_role: 607478818038480937 ...@@ -13,6 +13,7 @@ vote_role: 607478818038480937
tiebreaker_role: 607509283483025409 tiebreaker_role: 607509283483025409
unregistered_member_role: 608282247350714408 unregistered_member_role: 608282247350714408
registered_member_role: 608282133118582815 registered_member_role: 608282133118582815
expired_member_role: 0
command_prefix: "!" command_prefix: "!"
for_vote: "👍" for_vote: "👍"
......
...@@ -6,11 +6,12 @@ pub struct LDAPUser { ...@@ -6,11 +6,12 @@ pub struct LDAPUser {
pub username: String, pub username: String,
pub name: String, pub name: String,
pub when_created: String, pub when_created: String,
pub login_shell: String,
} }
pub fn ldap_search(username: &str) -> Option<LDAPUser> { pub fn ldap_search(username: &str) -> Option<LDAPUser> {
let settings = LdapConnSettings::new().set_no_tls_verify(true); let settings = LdapConnSettings::new().set_no_tls_verify(true);
let ldap = let mut ldap =
LdapConn::with_settings(settings, &CONFIG.bind_address).expect("Unable to connect to LDAP"); LdapConn::with_settings(settings, &CONFIG.bind_address).expect("Unable to connect to LDAP");
ldap.simple_bind( ldap.simple_bind(
"cn=ucc-discord-bot,cn=Users,dc=ad,dc=ucc,dc=gu,dc=uwa,dc=edu,dc=au", "cn=ucc-discord-bot,cn=Users,dc=ad,dc=ucc,dc=gu,dc=uwa,dc=edu,dc=au",
...@@ -23,14 +24,14 @@ pub fn ldap_search(username: &str) -> Option<LDAPUser> { ...@@ -23,14 +24,14 @@ pub fn ldap_search(username: &str) -> Option<LDAPUser> {
.search( .search(
"cn=Users,dc=ad,dc=ucc,dc=gu,dc=uwa,dc=edu,dc=au", "cn=Users,dc=ad,dc=ucc,dc=gu,dc=uwa,dc=edu,dc=au",
Scope::Subtree, Scope::Subtree,
&format!("(cn={})", username), &format!("(cn={})", ldap3::ldap_escape(username)),
vec!["when_created", "displayName", "name"], vec!["when_created", "displayName", "name", "loginShell"],
) )
.expect("LDAP error") .expect("LDAP error")
.success() .success()
.expect("LDAP search error"); .expect("LDAP search error");
if rs.is_empty() { if rs.is_empty() {
return None return None;
} }
let result = SearchEntry::construct(rs[0].clone()).attrs; let result = SearchEntry::construct(rs[0].clone()).attrs;
Some(LDAPUser { Some(LDAPUser {
...@@ -42,10 +43,14 @@ pub fn ldap_search(username: &str) -> Option<LDAPUser> { ...@@ -42,10 +43,14 @@ pub fn ldap_search(username: &str) -> Option<LDAPUser> {
.get("displayName") .get("displayName")
.expect("LDAP failed to get 'displayName' field") .expect("LDAP failed to get 'displayName' field")
.join(""), .join(""),
when_created: "".to_string() // result when_created: "".to_string(), // result
// .get("whenCreated") // .get("whenCreated")
// .expect("LDAP failed to get 'whenCreated' field") // .expect("LDAP failed to get 'whenCreated' field")
// .join(""), // .join(""),
login_shell: result
.get("loginShell")
.expect("LDAP failed to get 'loginShell' field")
.join(""),
}) })
} }
......
...@@ -12,15 +12,14 @@ extern crate guard; ...@@ -12,15 +12,14 @@ extern crate guard;
extern crate diesel; extern crate diesel;
extern crate ldap3; extern crate ldap3;
extern crate reqwest;
extern crate tokio;
use simplelog::*; use simplelog::*;
use std::fs::File; use std::fs::File;
use chrono::prelude::Utc; use serenity::client::Client;
use serenity::{
model::{channel, channel::Message, gateway::Ready, guild::Member},
prelude::*,
utils::MessageBuilder,
};
#[macro_use] #[macro_use]
mod util; mod util;
...@@ -28,192 +27,18 @@ mod config; ...@@ -28,192 +27,18 @@ mod config;
mod database; mod database;
mod ldap; mod ldap;
mod reaction_roles; mod reaction_roles;
mod serenity_handler;
mod token_management; mod token_management;
mod user_management; mod user_management;
mod voting; mod voting;
use config::{CONFIG, SECRETS}; use config::SECRETS;
use reaction_roles::{add_role_by_reaction, remove_role_by_reaction}; use serenity_handler::Handler;
use util::get_string_from_react;
struct Handler;
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) {
if !(msg.content.starts_with(&CONFIG.command_prefix)) {
return;
}
let message_content: Vec<_> = msg.content[1..].splitn(2, ' ').collect();
let content = if message_content.len() > 1 {
message_content[1]
} else {
""
};
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),
"logreact" => {
e!("Error deleting logreact prompt: {:?}", msg.delete(&ctx));
send_message!(
msg.channel_id,
&ctx.http,
"React to this to log the ID (for the next 5min)"
);
}
"help" => {
// Plaintext version, keep in case IRC users kick up a fuss
// let mut message = MessageBuilder::new();
// message.push_line(format!(
// "Use {}move <action> to make a circular motion",
// &CONFIG.command_prefix
// ));
// message.push_line(format!(
// "Use {}poll <proposal> to see what people think about something",
// &CONFIG.command_prefix
// ));
// send_message!(msg.channel_id, &ctx.http, message.build());
let result = msg.channel_id.send_message(&ctx.http, |m| {
m.embed(|embed| {
embed.colour(serenity::utils::Colour::DARK_GREY);
embed.title("Commands for the UCC Bot");
embed.field("About", "This is UCC's own little in-house bot, please treat it nicely :)", false);
embed.field("Commitee", "`!move <text>` to make a circular motion\n\
`!poll <text>` to get people's opinions on something", false);
embed.field("Account", "`!register <ucc username>` to link your Discord and UCC account\n\
`!profile <user>` to get the profile of a user\n\
`!set <bio|git|web|photo>` to set that property of _your_ profile", false);
embed.field("Fun", "`!cowsay <text>` to have a cow say your words\n\
with no `<text>` it'll give you a fortune 😉", false);
embed
});
m
});
if let Err(why) = result {
error!("Error sending help embed: {:?}", why);
}
}
// undocumented (in !help) functins
"ldap" => send_message!(
msg.channel_id,
&ctx.http,
format!("{:?}", ldap::ldap_search(message_content[1]))
),
"tla" => send_message!(
msg.channel_id,
&ctx.http,
format!("{:?}", ldap::tla_search(message_content[1]))
),
_ => send_message!(
msg.channel_id,
&ctx.http,
format!("Unrecognised command. Try {}help", &CONFIG.command_prefix)
),
}
}
fn reaction_add(&self, ctx: Context, add_reaction: channel::Reaction) {
match add_reaction.message(&ctx.http) {
Ok(message) => match get_message_type(&message) {
MessageType::RoleReactMessage if add_reaction.user_id.0 != 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 =>
{
return
}
MessageType::Motion => voting::reaction_add(ctx, add_reaction),
MessageType::LogReact => {
let react_user = add_reaction.user(&ctx).unwrap();
let react_as_string = get_string_from_react(&add_reaction.emoji);
if Utc::now().timestamp() - message.timestamp.timestamp() > 300 {
warn!(
"The logreact message {} just tried to use is too old",
react_user.name
);
return
}
info!(
"The react {} just added is {:?}. In full: {:?}",
react_user.name, react_as_string, add_reaction.emoji
);
let mut msg = MessageBuilder::new();
msg.push_italic(react_user.name);
msg.push(format!(
" wanted to know that {} is represented by ",
add_reaction.emoji,
));
msg.push_mono(react_as_string);
send_message!(message.channel_id, &ctx.http, msg.build());
}
_ => {}
},
Err(why) => error!("Failed to get react message {:?}", why),
}
}
fn reaction_remove(&self, ctx: Context, removed_reaction: channel::Reaction) {
match removed_reaction.message(&ctx.http) {
Ok(message) => match get_message_type(&message) {
MessageType::RoleReactMessage if removed_reaction.user_id != 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 =>
{
return
}
MessageType::Motion => voting::reaction_remove(ctx, removed_reaction),
_ => {}
},
Err(why) => error!("Failed to get react message {:?}", why),
}
}
fn guild_member_addition( #[tokio::main]
&self, async fn main() {
ctx: Context,
_guild_id: serenity::model::id::GuildId,
the_new_member: Member,
) {
user_management::new_member(&ctx, the_new_member);
}
// Set a handler to be called on the `ready` event. This is called when a
// shard is booted, and a READY payload is sent by Discord. This payload
// contains data like the current user's guild Ids, current user data,
// private channels, and more.
//
// In this case, just print what the current user's username is.
fn ready(&self, ctx: Context, ready: Ready) {
info!("{} is connected!", ready.user.name);
reaction_roles::sync_all_role_reactions(&ctx);
}
fn resume(&self, ctx: Context, _: serenity::model::event::ResumedEvent) {
reaction_roles::sync_all_role_reactions(&ctx);
}
}
fn main() {
CombinedLogger::init(vec![ CombinedLogger::init(vec![
TermLogger::new(LevelFilter::Info, Config::default(), TerminalMode::Mixed).unwrap(), TermLogger::new(LevelFilter::Info, Config::default(), TerminalMode::Mixed),
WriteLogger::new( WriteLogger::new(
LevelFilter::Info, LevelFilter::Info,
Config::default(), Config::default(),
...@@ -222,52 +47,21 @@ fn main() { ...@@ -222,52 +47,21 @@ fn main() {
]) ])
.unwrap(); .unwrap();
// ical::fetch_latest_ical().wait();
// Create a new instance of the Client, logging in as a bot. This will // 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 // automatically prepend your bot token with "Bot ", which is a requirement
// by Discord for bot users. // 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. // Finally, start a single shard, and start listening to events.
// //
// Shards will automatically attempt to reconnect, and will perform // Shards will automatically attempt to reconnect, and will perform
// exponential backoff until it reconnects. // exponential backoff until it reconnects.
if let Err(why) = client.start() { if let Err(why) = client.start().await {
error!("Client error: {:?}", why); error!("Client error: {:?}", why);
} }
} }
#[derive(Debug, PartialEq)]
enum MessageType {
Motion,
Role,
RoleReactMessage,
LogReact,
Poll,
Misc,
}
fn get_message_type(message: &Message) -> MessageType {
if CONFIG
.react_role_messages
.iter()
.any(|rrm| rrm.message == message.id)
{
return MessageType::RoleReactMessage;
}
if message.embeds.is_empty() {
// Get first word of message
return match message.content.splitn(2, ' ').next().unwrap() {
"Role" => MessageType::Role,
"React" => MessageType::LogReact,
_ => MessageType::Misc,
};
}
let title: String = message.embeds[0].title.clone().unwrap();
let words_of_title: Vec<_> = title.splitn(2, ' ').collect();
let first_word_of_title = words_of_title[0];
match first_word_of_title {
"Motion" => MessageType::Motion,
"Poll" => MessageType::Poll,
_ => MessageType::Misc,
}
}
use crate::config::{ReactRoleMap, CONFIG}; use crate::config::{ReactRoleMap, CONFIG};
use crate::util::{get_react_from_string, get_string_from_react}; use crate::util::{get_react_from_string, get_string_from_react};
use rayon::prelude::*;
use serenity::{ use serenity::{
client::Context, client::Context,
model::{channel::Message, channel::Reaction, id::RoleId, id::UserId}, model::{channel::Message, channel::Reaction, id::RoleId, id::UserId},
...@@ -8,10 +7,12 @@ use serenity::{ ...@@ -8,10 +7,12 @@ use serenity::{
use std::collections::{HashMap, HashSet}; use std::collections::{HashMap, HashSet};
use std::iter::FromIterator; 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 let user = added_reaction
.user_id .user_id
.unwrap()
.to_user(ctx) .to_user(ctx)
.await
.expect("Unable to get user"); .expect("Unable to get user");
if let Some(role_id) = CONFIG if let Some(role_id) = CONFIG
.react_role_messages .react_role_messages
...@@ -27,24 +28,29 @@ pub fn add_role_by_reaction(ctx: &Context, msg: Message, added_reaction: Reactio ...@@ -27,24 +28,29 @@ pub fn add_role_by_reaction(ctx: &Context, msg: Message, added_reaction: Reactio
user.name, user.name,
role_id role_id
.to_role_cached(ctx) .to_role_cached(ctx)
.await
.expect("Unable to get role") .expect("Unable to get role")
.name .name
); );
ctx.http ctx.http
.add_member_role( .add_member_role(
CONFIG.server_id, CONFIG.server_id,
added_reaction.user_id.0, *added_reaction.user_id.unwrap().as_u64(),
*role_id.as_u64(), *role_id.as_u64(),
) )
.await
.ok(); .ok();
} else { } else {
warn!("{} provided invalid react for role", user.name); 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) { pub async fn remove_role_by_reaction(ctx: &Context, msg: Message, removed_reaction: Reaction) {
CONFIG let role_id = CONFIG
.react_role_messages .react_role_messages
.iter() .iter()
.find(|rrm| rrm.message == msg.id) .find(|rrm| rrm.message == msg.id)
...@@ -52,30 +58,31 @@ pub fn remove_role_by_reaction(ctx: &Context, msg: Message, removed_reaction: Re ...@@ -52,30 +58,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); let react_as_string = get_string_from_react(&removed_reaction.emoji);
reaction_mapping.mapping.get(&react_as_string) reaction_mapping.mapping.get(&react_as_string)
}) })
.and_then(|role_id| { .unwrap();
info!( info!(
"{} requested removal of role '{}'", "{} requested removal of role '{}'",
msg.author.name, msg.author.name,
role_id role_id
.to_role_cached(ctx) .to_role_cached(ctx)
.expect("Unable to get role") .await
.name .expect("Unable to get role")
); .name
ctx.http );
.remove_member_role( ctx.http
CONFIG.server_id, .remove_member_role(
removed_reaction.user_id.0, CONFIG.server_id,
*role_id.as_u64(), *removed_reaction.user_id.unwrap().as_u64(),
) *role_id.as_u64(),
.ok() )
}); .await
.ok();
} }
pub fn sync_all_role_reactions(ctx: &Context) { pub async fn sync_all_role_reactions(ctx: &Context) {
info!("Syncing roles to reactions"); info!("Syncing roles to reactions");
let messages_with_role_mappings = get_all_role_reaction_message(ctx); let messages_with_role_mappings = get_all_role_reaction_message(ctx);
info!(" Sync: reaction messages fetched"); 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"); info!(" Sync: guild fetched");
// this method supports paging, but we probably don't need it since the server only has a couple of // 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 // hundred members. the Reaction.users() method can apparently only retrieve 100 users at once, but
...@@ -83,30 +90,42 @@ pub fn sync_all_role_reactions(ctx: &Context) { ...@@ -83,30 +90,42 @@ pub fn sync_all_role_reactions(ctx: &Context) {
let mut all_members = ctx let mut all_members = ctx
.http .http
.get_guild_members(CONFIG.server_id, Some(1000), None) .get_guild_members(CONFIG.server_id, Some(1000), None)
.await
.unwrap(); .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"); info!(" Sync: all members fetched");
let mut roles_to_add: HashMap<UserId, Vec<RoleId>> = 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>> = 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); info!(" Sync: prossessing message #{}", i);
for react in &message.reactions { for react in &message.reactions {
let react_as_string = get_string_from_react(&react.reaction_type); let react_as_string = get_string_from_react(&react.reaction_type);
if mapping.contains_key(&react_as_string) { if mapping.contains_key(&react_as_string) {
continue continue;
} }
info!( info!(
" message #{}: Removing non-role react '{}'", " message #{}: Removing non-role react '{}'",
i, react_as_string i, react_as_string
); );
for _illegal_react in for illegal_react_user in &message
&message.reaction_users(ctx, react.reaction_type.clone(), Some(100), None) .reaction_users(&ctx.http, react.reaction_type.clone(), Some(100), None)
.await
.unwrap_or(vec![])
{ {
warn!(" need to implement react removal"); message
.channel_id
.delete_reaction(
&ctx.http,
message.id,
Some(illegal_react_user.id),
react.reaction_type.clone(),
)
.await
.expect("Unable to delete react");
} }
} }
for (react, role) in *mapping { for (react, role) in *mapping {
...@@ -115,6 +134,7 @@ pub fn sync_all_role_reactions(ctx: &Context) { ...@@ -115,6 +134,7 @@ pub fn sync_all_role_reactions(ctx: &Context) {
let reaction_type = get_react_from_string(react.clone(), guild.clone()); let reaction_type = get_react_from_string(react.clone(), guild.clone());
let reactors = message let reactors = message
.reaction_users(ctx.http.clone(), reaction_type.clone(), Some(100), None) .reaction_users(ctx.http.clone(), reaction_type.clone(), Some(100), None)
.await
.unwrap(); .unwrap();
let reactor_ids: HashSet<UserId> = HashSet::from_iter(reactors.iter().map(|r| r.id)); let reactor_ids: HashSet<UserId> = HashSet::from_iter(reactors.iter().map(|r| r.id));
...@@ -122,12 +142,12 @@ pub fn sync_all_role_reactions(ctx: &Context) { ...@@ -122,12 +142,12 @@ pub fn sync_all_role_reactions(ctx: &Context) {
if !reactor_ids.contains(&UserId::from(CONFIG.bot_id)) { if !reactor_ids.contains(&UserId::from(CONFIG.bot_id)) {
e!( e!(
"Unable to add reaction, {:?}", "Unable to add reaction, {:?}",
message.react(ctx, reaction_type) message.react(ctx, reaction_type).await
); );
} }
for member in all_members.clone() { 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 reactor_ids.contains(&user_id) {
if !member.roles.iter().any(|r| r == role) { if !member.roles.iter().any(|r| r == role) {
roles_to_add.get_mut(&user_id).unwrap().push(*role); roles_to_add.get_mut(&user_id).unwrap().push(*role);
...@@ -144,10 +164,13 @@ pub fn sync_all_role_reactions(ctx: &Context) { ...@@ -144,10 +164,13 @@ pub fn sync_all_role_reactions(ctx: &Context) {
if !roles.is_empty() { if !roles.is_empty() {
let mut member = all_members let mut member = all_members
.iter() .iter()
.find(|m| m.user_id() == user_id) .find(|m| m.user.id == user_id)
.unwrap() .unwrap()
.clone(); .clone();
member.add_roles(ctx.http.clone(), &roles[..]).unwrap(); member
.add_roles(ctx.http.clone(), &roles[..])
.await
.unwrap();
} }
} }
info!(" Sync: (any) missing roles added"); info!(" Sync: (any) missing roles added");
...@@ -155,36 +178,36 @@ pub fn sync_all_role_reactions(ctx: &Context) { ...@@ -155,36 +178,36 @@ pub fn sync_all_role_reactions(ctx: &Context) {
if !roles.is_empty() { if !roles.is_empty() {
let mut member = all_members let mut member = all_members
.iter() .iter()
.find(|m| m.user_id() == user_id) .find(|m| m.user.id == user_id)
.unwrap() .unwrap()
.clone(); .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!(" Sync: (any) superflous roles removed");
info!("Role reaction sync complete"); info!("Role reaction sync complete");
} }
fn get_all_role_reaction_message(ctx: &Context) -> Vec<(Message, &'static ReactRoleMap)> { async fn get_all_role_reaction_message(ctx: &Context) -> Vec<(Message, &'static ReactRoleMap)> {
let guild = ctx.http.get_guild(CONFIG.server_id).unwrap(); let guild = ctx.http.get_guild(CONFIG.server_id).await.unwrap();
info!(" Find role-react message: guild determined"); 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"); info!(" Find role-react message: channels determined");
let http = ctx.http.clone(); let http = ctx.http.clone();
channels let mut v = Vec::new();
.par_iter() for channel in channels {
.flat_map(|channel| { for reaction in CONFIG.react_role_messages.iter() {
// since we don't know which channels the messages are in, we check every combination if let Some(m) = http
// of message and channel and ignore the bad matches using .ok() and .filter_map() .get_message(*channel.id.as_u64(), *reaction.message.as_u64())
let h = http.clone(); // thread-local copy .await
CONFIG .ok()
.react_role_messages {
.par_iter() v.push((m, &reaction.mapping))
.filter_map(move |rrm| { }
h.get_message(*channel.id.as_u64(), *rrm.message.as_u64()) }
.ok() }
.map(|m| (m, &rrm.mapping)) v
})
})
.collect()
} }
use async_trait::async_trait;
use chrono::prelude::Utc;
use serenity::{
model::{channel, channel::Message, gateway::Ready, guild::Member},
prelude::*,
utils::MessageBuilder,
};
// use rand::seq::SliceRandom;
use crate::config::CONFIG;
use crate::ldap;
use crate::reaction_roles::{
add_role_by_reaction, remove_role_by_reaction, sync_all_role_reactions,
};
use crate::user_management;
use crate::util::get_string_from_react;
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.
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))
// mobile mentions
{
send_message!(
msg.channel_id,
&ctx.http,
MENTION_RESPONSES[0] //.choose(&mut rand::random())
//.expect("We couldn't get any sass")
);
}
return;
}
let message_content: Vec<_> = msg.content[1..].splitn(2, ' ').collect();
let content = if message_content.len() > 1 {
message_content[1]
} else {
""
};
match message_content[0] {
"say" => println!("{:#?}", msg.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(
"You want to look at my insides!? Eurgh.\nJust kidding, you can go over ",
);
mesg.push_italic("every inch");
mesg.push(" of me here: https://gitlab.ucc.asn.au/UCC/discord-bot 😉");
send_message!(msg.channel_id, &ctx.http, mesg.build());
}
"help" => {
// Plaintext version, keep in case IRC users kick up a fuss
// let mut message = MessageBuilder::new();
// message.push_line(format!(
// "Use {}move <action> to make a circular motion",
// &CONFIG.command_prefix
// ));
// message.push_line(format!(
// "Use {}poll <proposal> to see what people think about something",
// &CONFIG.command_prefix
// ));
// send_message!(msg.channel_id, &ctx.http, message.build());
let result = msg.channel_id.send_message(&ctx.http, |m| {
m.embed(|embed| {
embed.colour(serenity::utils::Colour::DARK_GREY);
embed.title("Commands for the UCC Bot");
embed.field("About", "This is UCC's own little in-house bot, please treat it nicely :)", false);
embed.field("Commitee", "`!move <text>` to make a circular motion\n\
`!poll <text>` to get people's opinions on something", false);
embed.field("Account", "`!register <ucc username>` to link your Discord and UCC account\n\
`!profile <user>` to get the profile of a user\n\
`!set <bio|git|web|photo>` to set that property of _your_ profile\n\
`!updateroles` to update your registered roles", false);
embed.field("Fun", "`!cowsay <text>` to have a cow say your words\n\
with no `<text>` it'll give you a fortune 😉", false);
embed
});
m
});
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).await
);
send_message!(
msg.channel_id,
&ctx.http,
"React to this to log the ID (for the next 5min)"
);
}
"ldap" => send_message!(
msg.channel_id,
&ctx.http,
format!("{:?}", ldap::ldap_search(message_content[1]))
),
"tla" => send_message!(
msg.channel_id,
&ctx.http,
format!("{:?}", ldap::tla_search(message_content[1]))
),
"updateroles" => user_management::Commands::update_registered_role(ctx, msg).await,
_ => send_message!(
msg.channel_id,
&ctx.http,
format!("Unrecognised command. Try {}help", &CONFIG.command_prefix)
),
}
}
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 == Some(CONFIG.bot_id) => {
add_role_by_reaction(&ctx, message, add_reaction).await
}
_ if message.author.id != CONFIG.bot_id
|| add_reaction.user_id == Some(CONFIG.bot_id) => {}
MessageType::Motion => voting::reaction_add(&ctx, add_reaction).await,
MessageType::LogReact => {
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!(
"The logreact message {} just tried to use is too old",
react_user.name
);
return;
}
info!(
"The react {} just added is {:?}. In full: {:?}",
react_user.name, react_as_string, add_reaction.emoji
);
let mut msg = MessageBuilder::new();
msg.push_italic(react_user.name);
msg.push(format!(
" wanted to know that {} is represented by ",
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());
}
_ => {}
},
Err(why) => error!("Failed to get react message {:?}", why),
}
}
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.unwrap() != CONFIG.bot_id =>
{
remove_role_by_reaction(&ctx, message, removed_reaction).await
}
_ if message.author.id != CONFIG.bot_id
|| removed_reaction.user_id.unwrap() == CONFIG.bot_id => {}
MessageType::Motion => voting::reaction_remove(&ctx, removed_reaction).await,
_ => {}
},
Err(why) => error!("Failed to get react message {:?}", why),
}
}
async fn guild_member_addition(
&self,
ctx: Context,
_guild_id: serenity::model::id::GuildId,
the_new_member: Member,
) {
user_management::new_member(&ctx, the_new_member).await
}
// Set a handler to be called on the `ready` event. This is called when a
// shard is booted, and a READY payload is sent by Discord. This payload
// contains data like the current user's guild Ids, current user data,
// private channels, and more.
//
// In this case, just print what the current user's username is.
async fn ready(&self, ctx: Context, ready: Ready) {
info!("{} is connected!", ready.user.name);
sync_all_role_reactions(&ctx).await
}
async fn resume(&self, ctx: Context, _: serenity::model::event::ResumedEvent) {
sync_all_role_reactions(&ctx).await
}
}
pub const MENTION_RESPONSES: &[&str] = &[
"Oh hello there",
"Stop bothering me. I'm busy.",
"You know, I'm trying to keep track of this place. I don't need any more distractions.",
"Don't you have better things to do?",
"(sigh) what now?",
"Yes, yes, I know I'm brilliant",
"What do I need to do to catch a break around here? Eh.",
"Mmmmhmmm. I'm still around, don't mind me.",
"You know, some people would consider this rude. Luckily I'm not one of those people. In fact, I'm not even a person.",
"Perhaps try bothering someone else for a change."
];
#[derive(Debug, PartialEq)]
enum MessageType {
Motion,
Role,
RoleReactMessage,
LogReact,
Poll,
Misc,
}
fn get_message_type(message: &Message) -> MessageType {
if CONFIG
.react_role_messages
.iter()
.any(|rrm| rrm.message == message.id)
{
return MessageType::RoleReactMessage;
}
if message.embeds.is_empty() {
// Get first word of message
return match message.content.splitn(2, ' ').next().unwrap() {
"Role" => MessageType::Role,
"React" => MessageType::LogReact,
_ => MessageType::Misc,
};
}
let title: String = message.embeds[0].title.clone().unwrap();
let words_of_title: Vec<_> = title.splitn(2, ' ').collect();
let first_word_of_title = words_of_title[0];
match first_word_of_title {
"Motion" => MessageType::Motion,
"Poll" => MessageType::Poll,
_ => MessageType::Misc,
}
}
use aes::Aes128;
use base64; use base64;
use block_modes::block_padding::Pkcs7;
use block_modes::{BlockMode, Cbc};
use chrono::{prelude::Utc, DateTime}; use chrono::{prelude::Utc, DateTime};
use openssl::symm::{decrypt, encrypt, Cipher};
use rand::Rng; use rand::Rng;
use serenity::model::user::User; use serenity::model::user::User;
use std::str; use std::str;
type Aes128Cbc = Cbc<Aes128, Pkcs7>;
pub static TOKEN_LIFETIME: i64 = 300; // 5 minutes pub static TOKEN_LIFETIME: i64 = 300; // 5 minutes
lazy_static! { lazy_static! {
static ref KEY: [u8; 32] = rand::thread_rng().gen::<[u8; 32]>(); static ref KEY: [u8; 32] = rand::thread_rng().gen::<[u8; 32]>();
static ref CIPHER: Cipher = Cipher::aes_256_cbc(); static ref IV: [u8; 16] = [0; 16];
static ref CIPHER: Aes128Cbc = Aes128Cbc::new_var(KEY.as_ref(), IV.as_ref()).unwrap();
} }
fn text_encrypt(plaintext: &str) -> String { fn text_encrypt(plaintext: &str) -> String {
let iv: &[u8; 16] = &[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; base64::encode(CIPHER.clone().encrypt_vec(plaintext.as_bytes()))
let encrypted_vec =
encrypt(*CIPHER, &*KEY, Some(iv), plaintext.as_bytes()).expect("encryption failed");
base64::encode(encrypted_vec.as_slice())
} }
fn text_decrypt(ciphertext: &str) -> Option<String> { fn text_decrypt(ciphertext: &str) -> Option<String> {
let iv: &[u8; 16] = &[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
guard!(let Ok(cipher_vec) = base64::decode(ciphertext) else { guard!(let Ok(cipher_vec) = base64::decode(ciphertext) else {
warn!("Unable to decode base64 text"); warn!("Unable to decode base64 text");
return None return None
}); });
guard!(let Ok(decrypted_vec) = decrypt(*CIPHER, &*KEY, Some(iv), &cipher_vec) else { guard!(let Ok(decrypted_vec) = CIPHER.clone().decrypt_vec(&cipher_vec) else {
warn!("Text decryption failed"); warn!("Text decryption failed");
return None return None
}); });
...@@ -75,7 +76,7 @@ pub fn parse_token(discord_user: &User, encrypted_token: &str) -> Result<String, ...@@ -75,7 +76,7 @@ pub fn parse_token(discord_user: &User, encrypted_token: &str) -> Result<String,
let token_username = token_components[2]; let token_username = token_components[2];
if token_discord_user != discord_user.id.0.to_string() { if token_discord_user != discord_user.id.0.to_string() {
warn!("... attempt failed : DiscordID mismatch"); warn!("... attempt failed : DiscordID mismatch");
return Err(TokenError::DiscordIdMismatch) return Err(TokenError::DiscordIdMismatch);
} }
let time_delta_seconds = Utc::now().timestamp() - token_timestamp.timestamp(); let time_delta_seconds = Utc::now().timestamp() - token_timestamp.timestamp();
if time_delta_seconds > TOKEN_LIFETIME { if time_delta_seconds > TOKEN_LIFETIME {
...@@ -83,7 +84,7 @@ pub fn parse_token(discord_user: &User, encrypted_token: &str) -> Result<String, ...@@ -83,7 +84,7 @@ pub fn parse_token(discord_user: &User, encrypted_token: &str) -> Result<String,
"... attempt failed : token expired ({} seconds old)", "... attempt failed : token expired ({} seconds old)",
time_delta_seconds time_delta_seconds
); );
return Err(TokenError::TokenExpired) return Err(TokenError::TokenExpired);
} }
info!( info!(
"... verification successful (token {} seconds old)", "... verification successful (token {} seconds old)",
......
use rand::seq::SliceRandom;
use regex::Regex; use regex::Regex;
use serenity::{ use serenity::{
model::{channel::Message, guild::Member}, model::{channel::Message, guild::Member, id::RoleId},
prelude::*, prelude::*,
utils::MessageBuilder, utils::MessageBuilder,
}; };
...@@ -10,21 +9,22 @@ use url::Url; ...@@ -10,21 +9,22 @@ use url::Url;
use crate::config::CONFIG; use crate::config::CONFIG;
use crate::database; use crate::database;
use crate::ldap::ldap_exists; use crate::ldap::{ldap_exists, ldap_search};
use crate::token_management::*; 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(); let mut message = MessageBuilder::new();
message.push("Nice to see you here "); message.push("Nice to see you here ");
message.mention(&new_member); message.mention(&new_member);
message.push_line("! Would you care to introduce yourself?"); message.push_line("! Would you care to introduce yourself?");
message.push_line("If you're not sure where to start, perhaps you could tell us about your projects, your first computer…"); message.push_line("If you're not sure where to start, perhaps you could tell us about your projects, your first computer…");
message.push_line("You should also know that we follow the Freenode Channel Guidelines: https://freenode.net/changuide, and try to avoid defamatory content."); message.push_line("You should also know that we follow the Freenode Channel Guidelines: https://freenode.net/changuide, and try to avoid defamatory content.");
message.push_line("Make sure to check out "); message.push("Make sure to check out ");
message.mention(&CONFIG.readme_channel); message.mention(&CONFIG.readme_channel);
message.push_line(" to get yourself some roles for directed pings 😊, and "); message.push(" to get yourself some roles for directed pings 😊, and ");
message.push_mono(format!("{}register username", CONFIG.command_prefix)); message.push_mono(format!("{}register username", CONFIG.command_prefix));
message.push_line(" to link to your UCC account."); message.push(" to link to your UCC account.");
send_message!(CONFIG.welcome_channel, &ctx, message.build()); send_message!(CONFIG.welcome_channel, &ctx, message.build());
let mut message = MessageBuilder::new(); let mut message = MessageBuilder::new();
...@@ -32,7 +32,10 @@ pub fn new_member(ctx: &Context, mut new_member: Member) { ...@@ -32,7 +32,10 @@ pub fn new_member(ctx: &Context, mut new_member: Member) {
message.mention(&CONFIG.welcome_channel); message.mention(&CONFIG.welcome_channel);
send_message!(CONFIG.main_channel, &ctx, message.build()); 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); error!("Error adding user role: {:?}", why);
} }
} }
...@@ -71,24 +74,23 @@ pub const RESERVED_NAMES: &[&str] = &[ ...@@ -71,24 +74,23 @@ pub const RESERVED_NAMES: &[&str] = &[
pub struct Commands; pub struct Commands;
impl 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() { if account_name.is_empty() {
send_message!( send_message!(
msg.channel_id, msg.channel_id,
&ctx.http, &ctx.http,
format!("Usage: {}register <username>", CONFIG.command_prefix) format!("Usage: {}register <username>", CONFIG.command_prefix)
); );
return return;
} }
if RESERVED_NAMES.contains(&account_name) || database::username_exists(account_name) { if RESERVED_NAMES.contains(&account_name) || database::username_exists(account_name) {
send_message!( send_message!(
msg.channel_id, msg.channel_id,
&ctx.http, &ctx.http,
RANDOM_SASS RANDOM_SASS[0] //.choose(&mut rand::thread_rng())
.choose(&mut rand::thread_rng()) //.expect("We couldn't get any sass")
.expect("We couldn't get any sass")
); );
return return;
} }
if !ldap_exists(account_name) { if !ldap_exists(account_name) {
send_message!( send_message!(
...@@ -99,7 +101,7 @@ impl Commands { ...@@ -99,7 +101,7 @@ impl Commands {
account_name account_name
) )
); );
return return;
} }
send_message!( send_message!(
msg.channel_id, msg.channel_id,
...@@ -110,7 +112,10 @@ impl Commands { ...@@ -110,7 +112,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>\ 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\ <p>Hi {}, to complete the link, go to the discord server and enter\
...@@ -132,40 +137,106 @@ impl Commands { ...@@ -132,40 +137,106 @@ impl Commands {
Err(why) => error!("Unable to send message with mutt {:?}", why), Err(why) => error!("Unable to send message with mutt {:?}", why),
}; };
} }
pub fn verify(ctx: Context, msg: Message, token: &str) {
pub fn get_registered_role(name: String) -> Option<u64> {
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);
}
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 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 {
return // Err()
});
guard!(let Ok(mut discord_member) = serenity::model::id::GuildId(CONFIG.server_id)
.member(ctx.http.clone(), msg.author.id).await else {
return // Err()
});
let roles_to_remove = vec![
CONFIG.registered_member_role,
CONFIG.unregistered_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 discord_member.roles.contains(&RoleId::from(role))
&& discord_member.remove_role(&ctx.http, role).await.is_err()
{
return; // Err()
}
}
if !discord_member
.roles
.contains(&RoleId::from(registered_role))
&& discord_member
.add_role(&ctx.http, registered_role)
.await
.is_err()
{
return; // Err()
}
// Ok()
}
pub async fn verify(ctx: Context, msg: Message, token: &str) {
match parse_token(&msg.author, token) { match parse_token(&msg.author, token) {
Ok(name) => { Ok(name) => {
e!( if let Ok(mut member) = serenity::model::id::GuildId(CONFIG.server_id)
"Unable to get member: {:?}", .member(ctx.http.clone(), msg.author.id)
serenity::model::id::GuildId(CONFIG.server_id) .await
.member(ctx.http.clone(), msg.author.id) {
.map(|mut member| { let full_member = database::add_member(&msg.author.id.0, &name);
let full_member = database::add_member(&msg.author.id.0, &name); e!(
e!( "Unable to remove role: {:?}",
"Unable to remove role: {:?}", member
member.remove_role(&ctx.http, CONFIG.unregistered_member_role) .remove_role(&ctx.http, CONFIG.unregistered_member_role)
); .await
e!( );
"Unable to add role: {:?}", guard!(let Some(member_role) = Commands::get_registered_role(name) else {
member.add_role(&ctx.http, CONFIG.registered_member_role) send_message!(msg.channel_id, ctx.http.clone(), "Couldn't find you in LDAP!");
); return
e!( });
"Unable to edit nickname: {:?}", e!(
member.edit(&ctx.http, |m| { "Unable to add role: {:?}",
m.nickname(member_nickname(&full_member)); member.add_role(&ctx.http, member_role).await
m );
}) e!(
); "Unable to edit nickname: {:?}",
let mut verification_message = MessageBuilder::new(); member
verification_message.push(format!("Great, {}! Verification was successful. To provide a friendly introduction to yourself, consider doing ", &full_member.username)); .edit(&ctx.http, |m| {
verification_message.push_mono(format!("{}set bio <info>", CONFIG.command_prefix)); m.nickname(member_nickname(&full_member));
send_message!( m
msg.channel_id, })
ctx.http.clone(), .await
verification_message.build() );
); let mut verification_message = MessageBuilder::new();
}) verification_message.push(format!("Great, {}! Verification was successful. To provide a friendly introduction to yourself, consider doing ", &full_member.username));
); verification_message
.push_mono(format!("{}set bio <info>", CONFIG.command_prefix));
send_message!(
msg.channel_id,
ctx.http.clone(),
verification_message.build()
);
} else {
error!("Unable to get member: {:?}", name)
}
} }
Err(reason) => send_message!( Err(reason) => send_message!(
msg.channel_id, msg.channel_id,
...@@ -173,20 +244,32 @@ impl Commands { ...@@ -173,20 +244,32 @@ impl Commands {
format!("Verification error: {:?}", reason) 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() { let possible_member: Option<database::Member> = match if name.trim().is_empty() {
info!(
"{} (discord name) wants to look at their own profile",
&msg.author.name
);
database::get_member_info(&msg.author.id.0) database::get_member_info(&msg.author.id.0)
} else { } else {
info!("Searching for a profile for {}", &name);
database::get_member_info_from_username(&name) database::get_member_info_from_username(&name)
} { } {
Ok(member) => Some(member), Ok(member) => Some(member),
Err(why) => { Err(why) => {
warn!("Could not find member {:?}", why); warn!("Could not find member {}, {:?}", &name, why);
if name.len() != 3 { if name.len() != 3 {
None None
} else { } else {
info!(
"Searching for a profile for the TLA '{}'",
&name.to_uppercase()
);
match database::get_member_info_from_tla(&name.to_uppercase()) { match database::get_member_info_from_tla(&name.to_uppercase()) {
Ok(member) => Some(member), Ok(member) => Some(member),
Err(_) => None, Err(_) => None,
...@@ -200,21 +283,25 @@ impl Commands { ...@@ -200,21 +283,25 @@ impl Commands {
&ctx.http, &ctx.http,
"Sorry, I couldn't find that profile (you need to !register for a profile)" "Sorry, I couldn't find that profile (you need to !register for a profile)"
); );
return return;
} }
let member = possible_member.unwrap(); let member = possible_member.unwrap();
info!("Found matching profile, UCC username: {}", &member.username);
let user = match ctx.http.get_user(member.discord_id as _).await {
Ok(u) => u,
Err(e) => {
error!("Couldn't find matching Discord ID for username! {:?}", e);
return;
}
};
let result = msg.channel_id.send_message(&ctx.http, |m| { let result = msg.channel_id.send_message(&ctx.http, |m| {
m.embed(|embed| { m.embed(|embed| {
embed.colour(serenity::utils::Colour::LIGHTER_GREY); embed.colour(serenity::utils::Colour::LIGHTER_GREY);
embed.footer(|f| { embed.footer(|f| {
let user = &ctx
.http
.get_user(member.discord_id.clone() as u64)
.expect("We expected this user to exist... they didn't ;(");
f.text(&user.name); f.text(&user.name);
f.icon_url( f.icon_url(
user.static_avatar_url() user.static_avatar_url()
.expect("Expected user to have avatar"), .unwrap_or(String::from("https://www.ucc.asn.au/logos/ucc-logo.png")),
); );
f f
}); });
...@@ -244,11 +331,11 @@ impl Commands { ...@@ -244,11 +331,11 @@ impl Commands {
}); });
m m
}); });
if let Err(why) = result { if let Err(why) = result.await {
error!("Error sending profile embed: {:?}", why); 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() { if info.trim().is_empty() {
msg.channel_id msg.channel_id
.send_message(&ctx.http, |m| { .send_message(&ctx.http, |m| {
...@@ -270,9 +357,9 @@ impl Commands { ...@@ -270,9 +357,9 @@ impl Commands {
embed embed
}); });
m m
}) }).await
.expect("Failed to send usage help embed"); .expect("Failed to send usage help embed");
return return;
} }
let info_content: Vec<_> = info.splitn(2, ' ').collect(); let info_content: Vec<_> = info.splitn(2, ' ').collect();
let mut property = String::from(info_content[0]); let mut property = String::from(info_content[0]);
...@@ -315,8 +402,9 @@ impl Commands { ...@@ -315,8 +402,9 @@ impl Commands {
}); });
m m
}) })
.await
.expect("Failed to send usage embed"); .expect("Failed to send usage embed");
return return;
} }
let mut value = info_content[1].to_string(); let mut value = info_content[1].to_string();
...@@ -333,11 +421,11 @@ impl Commands { ...@@ -333,11 +421,11 @@ impl Commands {
&ctx.http, &ctx.http,
"That ain't a URL where I come from..." "That ain't a URL where I come from..."
); );
return return;
} }
} }
} }
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!( send_message!(
msg.channel_id, msg.channel_id,
&ctx.http, &ctx.http,
...@@ -361,6 +449,7 @@ impl Commands { ...@@ -361,6 +449,7 @@ impl Commands {
info!( info!(
"Set {}'s {} in profile to {}", "Set {}'s {} in profile to {}",
&msg.author_nick(ctx.http.clone()) &msg.author_nick(ctx.http.clone())
.await
.unwrap_or(String::from("?")), .unwrap_or(String::from("?")),
property, property,
value value
...@@ -396,15 +485,14 @@ impl Commands { ...@@ -396,15 +485,14 @@ impl Commands {
send_message!(msg.channel_id, &ctx.http, "Failed to set property. Ooops."); send_message!(msg.channel_id, &ctx.http, "Failed to set property. Ooops.");
} }
} }
if let Err(why) = msg.delete(&ctx) { if let Err(why) = msg.delete(&ctx).await {
error!("Error deleting set profile property: {:?}", why); error!("Error deleting set profile property: {:?}", why);
} }
} }
pub fn clear_info(ctx: Context, msg: Message, field: &str) { pub async fn clear_info(ctx: Context, msg: Message, field: &str) {
if field.trim().is_empty() { if field.trim().is_empty() {
// just show the help page from set_info // just show the help page from set_info
Commands::set_info(ctx, msg, ""); return Commands::set_info(ctx, msg, "").await;
return;
} }
match field { match field {
"bio" => database::set_member_bio(&msg.author.id.0, None), "bio" => database::set_member_bio(&msg.author.id.0, None),
...@@ -417,7 +505,7 @@ impl Commands { ...@@ -417,7 +505,7 @@ impl Commands {
.expect("Unable to clear profile field"); .expect("Unable to clear profile field");
info!( info!(
"Cleared {}'s {} in profile", "Cleared {}'s {} in profile",
&msg.author_nick(ctx.http).unwrap_or(String::from("?")), &msg.author_nick(ctx.http).await.unwrap_or(String::from("?")),
field, field,
); );
} }
......
use serenity::model::{channel::ReactionType, guild::PartialGuild}; use std::str::FromStr;
use serenity::model::{channel::ReactionType, guild::PartialGuild, misc::EmojiIdentifier};
pub fn get_string_from_react(react: &ReactionType) -> String { pub fn get_string_from_react(react: &ReactionType) -> String {
match react { match react {
...@@ -17,7 +19,12 @@ pub fn get_react_from_string(string: String, guild: PartialGuild) -> ReactionTyp ...@@ -17,7 +19,12 @@ pub fn get_react_from_string(string: String, guild: PartialGuild) -> ReactionTyp
.values() .values()
.find(|e| e.name == string) .find(|e| e.name == string)
.map_or_else( .map_or_else(
|| ReactionType::from(string), // unicode emoji || {
ReactionType::from(EmojiIdentifier::from_str(&string).expect(&format!(
"Emoji string \"{}\" could not be identified",
&string
)))
}, // unicode emoji
|custom_emoji| ReactionType::from(custom_emoji.clone()), |custom_emoji| ReactionType::from(custom_emoji.clone()),
) )
} }
...@@ -35,7 +42,7 @@ macro_rules! e { ...@@ -35,7 +42,7 @@ macro_rules! e {
#[macro_use] #[macro_use]
macro_rules! send_message { macro_rules! send_message {
($chan:expr, $context:expr, $message:expr) => { ($chan:expr, $context:expr, $message:expr) => {
match $chan.say($context, $message) { match $chan.say($context, $message).await {
Ok(_) => (), Ok(_) => (),
Err(why) => error!("Error sending message: {:?}", why), Err(why) => error!("Error sending message: {:?}", why),
} }
...@@ -46,7 +53,7 @@ macro_rules! send_message { ...@@ -46,7 +53,7 @@ macro_rules! send_message {
#[macro_use] #[macro_use]
macro_rules! send_delete_message { macro_rules! send_delete_message {
($chan:expr, $context:expr, $message:expr) => { ($chan:expr, $context:expr, $message:expr) => {
match $chan.say($context, $message) { match $chan.say($context, $message).await {
Ok(the_new_msg) => e!( Ok(the_new_msg) => e!(
"Error deleting register message: {:?}", "Error deleting register message: {:?}",
the_new_msg.delete($context) the_new_msg.delete($context)
......
This diff is collapsed.