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 (8)
image: 'rust:latest'
stages:
- test
- build
variables:
CARGO_HOME: $CI_PROJECT_DIR/cargo
test:
stage: test
script:
- rustc --version
- cargo --version
- cargo test --verbose
build:
stage: build
script:
- cargo build --all --verbose
cache:
paths:
- cargo/
- target/
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "(gdb) Launch",
"type": "gdb",
"request": "launch",
"env": { "RUST_BACKTRACE": 1 },
"target": "${workspaceFolder}/target/debug/ucc-discord-bot",
"cwd": "${workspaceFolder}",
"gdbpath": "/home/tec/.cargo/bin/rust-gdb",
"arguments": ""
}
]
}
...@@ -5,13 +5,13 @@ authors = ["tec <tec@ucc.gu.uwa.edu.au>"] ...@@ -5,13 +5,13 @@ authors = ["tec <tec@ucc.gu.uwa.edu.au>"]
edition = "2018" edition = "2018"
[dependencies] [dependencies]
base64 = "^0.11"
chrono = "^0.4.10" chrono = "^0.4.10"
lazy_static = "^1.4.0" lazy_static = "^1.4.0"
log = "^0.4.8" log = "^0.4.8"
openssl = "^0.10"
rand = "^0.7.2" rand = "^0.7.2"
serde = "^1.0.104" serde = "^1.0.104"
serde_yaml = "^0.8" serde_yaml = "^0.8"
serenity = "0.8.0" serenity = "0.8.0"
simplelog = "^0.7.4" simplelog = "^0.7.4"
openssl = "^0.10"
base64 = "^0.11"
#+TITLE: UCC Discord Bot
This is a general-purpose bot build for use on the UCC discord.
* Current features
- Friendly welcome messages
- Make polls
- Pass circular motions
- Assign roles based on reacts
- Cowsay/fortune
* (Hopefully) Upcoming features
- LDAP/AD integration
- Sync roles with user groups
- Do stuff for/as users (e.g. dispense)
#+BEGIN_SRC
_________________________________
/ Tonight's the night: Sleep in a \
\ eucalyptus tree. /
---------------------------------
\
\
,__, | |
(oo)\| |___
(__)\| | )\_
| |_w | \
| | || *
Cower....
#+END_SRC
...@@ -7,10 +7,10 @@ _Involves: File r/w + parsing, discord reactions_ ...@@ -7,10 +7,10 @@ _Involves: File r/w + parsing, discord reactions_
So, for reaction roles, afaict this is what needs to be done So, for reaction roles, afaict this is what needs to be done
- [X] Migrate config.rs to something like ~config.yml~ - [X] Migrate config.rs to something like ~config.yml~
- [-] Complete ~reaction_roles.rs~ - [X] Complete ~reaction_roles.rs~
- [X] Load from config (roles, and the rr msg, if they exist) - [X] Load from config (roles, and the rr msg, if they exist)
- [X] Monitor reactions, update user roles etc. - [X] Monitor reactions, update user roles etc.
- [ ] On updated to ~config.yml~ (and on bot load, now that I think of it) overwrite the rr msg with content based on roles in config - [X] Only allow reacts which correspond to roles
* LDAP Integration * LDAP Integration
......
...@@ -20,8 +20,10 @@ disapprove_react: "⬇" ...@@ -20,8 +20,10 @@ disapprove_react: "⬇"
unsure_react: "❔" unsure_react: "❔"
react_role_messages: react_role_messages:
- message: 673400351277187072 - message: 674164298632790026
mapping: mapping:
🐊: 609708723006472198 # Autonomous Alligators 🐊: 609708723006472198 # Autonomous Alligators
🐃: 609708839243087892 # Bipedal Bison 🐃: 609708839243087892 # Bipedal Bison
🦆: 609708889763479562 # Omnipresent Ostriches 🦆: 609708889763479562 # Omnipresent Ostriches
j5: 607478818038480937 # Vote role
👻: 634415546804338688 # Background charachter
...@@ -53,9 +53,6 @@ impl EventHandler for Handler { ...@@ -53,9 +53,6 @@ impl EventHandler for Handler {
} }
"register" => user_management::Commands::register(ctx, msg.clone(), message_content[1]), "register" => user_management::Commands::register(ctx, msg.clone(), message_content[1]),
"verify" => user_management::Commands::verify(ctx, msg.clone(), message_content[1]), "verify" => user_management::Commands::verify(ctx, msg.clone(), message_content[1]),
"join" => {
user_management::Commands::join(ctx, msg.clone(), message_content[1]);
}
"move" => { "move" => {
voting::Commands::move_something(ctx, msg.clone(), message_content[1]); voting::Commands::move_something(ctx, msg.clone(), message_content[1]);
} }
...@@ -107,7 +104,9 @@ impl EventHandler for Handler { ...@@ -107,7 +104,9 @@ impl EventHandler for Handler {
match add_reaction.message(&ctx.http) { match add_reaction.message(&ctx.http) {
Ok(message) => { Ok(message) => {
let message_type = get_message_type(&message); let message_type = get_message_type(&message);
if message_type == MessageType::RoleReactMessage { if message_type == MessageType::RoleReactMessage
&& add_reaction.user_id.0 != CONFIG.bot_id
{
add_role_by_reaction(ctx, message, add_reaction); add_role_by_reaction(ctx, message, add_reaction);
return; return;
} }
...@@ -120,7 +119,7 @@ impl EventHandler for Handler { ...@@ -120,7 +119,7 @@ impl EventHandler for Handler {
} }
MessageType::LogReact => { MessageType::LogReact => {
let react_user = add_reaction.user(&ctx).unwrap(); let react_user = add_reaction.user(&ctx).unwrap();
let react_as_string = get_string_from_react(add_reaction.emoji.clone()); let react_as_string = get_string_from_react(&add_reaction.emoji);
if Utc::now().timestamp() - message.timestamp.timestamp() > 300 { if Utc::now().timestamp() - message.timestamp.timestamp() > 300 {
warn!( warn!(
"The logreact message {} just tried to use is too old", "The logreact message {} just tried to use is too old",
...@@ -129,8 +128,8 @@ impl EventHandler for Handler { ...@@ -129,8 +128,8 @@ impl EventHandler for Handler {
return; return;
} }
info!( info!(
"The react {} just added is {:?}", "The react {} just added is {:?}. In full: {:?}",
react_user.name, react_as_string react_user.name, react_as_string, add_reaction.emoji
); );
let mut msg = MessageBuilder::new(); let mut msg = MessageBuilder::new();
msg.push_italic(react_user.name); msg.push_italic(react_user.name);
...@@ -155,7 +154,9 @@ impl EventHandler for Handler { ...@@ -155,7 +154,9 @@ impl EventHandler for Handler {
match removed_reaction.message(&ctx.http) { match removed_reaction.message(&ctx.http) {
Ok(message) => { Ok(message) => {
let message_type = get_message_type(&message); let message_type = get_message_type(&message);
if message_type == MessageType::RoleReactMessage { if message_type == MessageType::RoleReactMessage
&& removed_reaction.user_id != CONFIG.bot_id
{
remove_role_by_reaction(ctx, message, removed_reaction); remove_role_by_reaction(ctx, message, removed_reaction);
return; return;
} }
......
...@@ -2,25 +2,53 @@ use crate::config::CONFIG; ...@@ -2,25 +2,53 @@ use crate::config::CONFIG;
use crate::util::{get_react_from_string, get_string_from_react}; use crate::util::{get_react_from_string, get_string_from_react};
use serenity::{ use serenity::{
client::Context, client::Context,
model::{channel::Message, channel::Reaction, id::UserId, id::RoleId}, model::{channel::Message, channel::Reaction, id::RoleId, id::UserId},
}; };
use std::collections::{HashMap, HashSet}; use std::collections::{HashMap, HashSet};
use std::iter::FromIterator; use std::iter::FromIterator;
macro_rules! e {
($error: literal, $x:expr) => {
match $x {
Ok(_) => (),
Err(why) => error!($error, why),
}
};
}
pub fn add_role_by_reaction(ctx: Context, msg: Message, added_reaction: Reaction) { pub fn add_role_by_reaction(ctx: Context, msg: Message, added_reaction: Reaction) {
CONFIG let user = added_reaction
.user_id
.to_user(&ctx)
.expect("Unable to get user");
if let Some(role_id) = CONFIG
.react_role_messages .react_role_messages
.iter() .iter()
.find(|rrm| rrm.message == msg.id) .find(|rrm| rrm.message == msg.id)
.and_then(|reaction_mapping| { .and_then(|reaction_mapping| {
let react_as_string = get_string_from_react(added_reaction.emoji); let react_as_string = get_string_from_react(&added_reaction.emoji);
reaction_mapping.mapping.get(&react_as_string) reaction_mapping.mapping.get(&react_as_string)
}) })
.and_then(|role_id| { {
ctx.http info!(
.add_member_role(CONFIG.server_id, *msg.author.id.as_u64(), *role_id.as_u64()) "{} requested role '{}'",
.ok() user.name,
}); role_id
.to_role_cached(&ctx)
.expect("Unable to get role")
.name
);
ctx.http
.add_member_role(
CONFIG.server_id,
added_reaction.user_id.0,
*role_id.as_u64(),
)
.ok();
} else {
warn!("{} provided invalid react for role", user.name);
e!("Unable to delete react: {:?}", added_reaction.delete(&ctx));
}
} }
pub fn remove_role_by_reaction(ctx: Context, msg: Message, removed_reaction: Reaction) { pub fn remove_role_by_reaction(ctx: Context, msg: Message, removed_reaction: Reaction) {
...@@ -29,19 +57,34 @@ pub fn remove_role_by_reaction(ctx: Context, msg: Message, removed_reaction: Rea ...@@ -29,19 +57,34 @@ pub fn remove_role_by_reaction(ctx: Context, msg: Message, removed_reaction: Rea
.iter() .iter()
.find(|rrm| rrm.message == msg.id) .find(|rrm| rrm.message == msg.id)
.and_then(|reaction_mapping| { .and_then(|reaction_mapping| {
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| { .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 ctx.http
.remove_member_role(CONFIG.server_id, *msg.author.id.as_u64(), *role_id.as_u64()) .remove_member_role(
CONFIG.server_id,
removed_reaction.user_id.0,
*role_id.as_u64(),
)
.ok() .ok()
}); });
} }
pub fn sync_all_role_reactions(ctx: Context) { pub fn sync_all_role_reactions(ctx: Context) {
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");
let guild = ctx.http.get_guild(CONFIG.server_id).unwrap(); let guild = ctx.http.get_guild(CONFIG.server_id).unwrap();
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
// this one seems to work fine when set to 1000 (I tried 10,000 but the api returned a 400) // this one seems to work fine when set to 1000 (I tried 10,000 but the api returned a 400)
...@@ -49,21 +92,49 @@ pub fn sync_all_role_reactions(ctx: Context) { ...@@ -49,21 +92,49 @@ pub fn sync_all_role_reactions(ctx: Context) {
.http .http
.get_guild_members(CONFIG.server_id, Some(1000), None) .get_guild_members(CONFIG.server_id, Some(1000), None)
.unwrap(); .unwrap();
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()))); let mut roles_to_add: 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())));
let mut roles_to_remove: HashMap<UserId, Vec<RoleId>> =
HashMap::from_iter(all_members.iter().map(|m| (m.user_id(), Vec::new())));
let mut i = 0;
for (message, mapping) in messages_with_role_mappings { for (message, mapping) in messages_with_role_mappings {
i += 1;
info!(" Sync: prossessing message #{}", i);
for react in &message.reactions {
let react_as_string = get_string_from_react(&react.reaction_type);
if mapping.contains_key(&react_as_string) {
continue;
}
info!(
" message #{}: Removing non-role react '{}'",
i, react_as_string
);
for _illegal_react in
&message.reaction_users(&ctx, react.reaction_type.clone(), Some(100), None)
{
warn!(" need to implement react removal");
}
}
for (react, role) in mapping { for (react, role) in mapping {
// the docs say this method can't retrieve more than 100 user reactions at a time, but it seems info!(" message #{}: processing react '{}'", i, react);
// to work fine when set to 255...
// TODO: proper pagination for the unlikely scenario that there are more than 100 (255?) reactions? // TODO: proper pagination for the unlikely scenario that there are more than 100 (255?) reactions?
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, Some(255), None) .reaction_users(ctx.http.clone(), reaction_type.clone(), Some(100), None)
.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));
// ensure bot has reacted
if !reactor_ids.contains(&UserId::from(CONFIG.bot_id)) {
e!(
"Unable to add reaction, {:?}",
message.react(&ctx, reaction_type)
);
}
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) {
...@@ -71,24 +142,36 @@ pub fn sync_all_role_reactions(ctx: Context) { ...@@ -71,24 +142,36 @@ pub fn sync_all_role_reactions(ctx: Context) {
roles_to_add.get_mut(&user_id).unwrap().push(*role); roles_to_add.get_mut(&user_id).unwrap().push(*role);
} }
} else if member.roles.iter().any(|r| r == role) { } else if member.roles.iter().any(|r| r == role) {
roles_to_remove.get_mut(&user_id).unwrap().push(*role); roles_to_remove.get_mut(&user_id).unwrap().push(*role);
} }
} }
} }
} }
info!(" Sync: finished determing roles to add/remove");
for (user_id, roles) in roles_to_add { for (user_id, roles) in roles_to_add {
if !roles.is_empty() { if !roles.is_empty() {
let mut member = all_members.iter().find(|m| m.user_id() == user_id).unwrap().clone(); let mut member = all_members
.iter()
.find(|m| m.user_id() == user_id)
.unwrap()
.clone();
member.add_roles(ctx.http.clone(), &roles[..]).unwrap(); member.add_roles(ctx.http.clone(), &roles[..]).unwrap();
} }
} }
info!(" Sync: (any) missing roles added");
for (user_id, roles) in roles_to_remove { for (user_id, roles) in roles_to_remove {
if !roles.is_empty() { if !roles.is_empty() {
let mut member = all_members.iter().find(|m| m.user_id() == user_id).unwrap().clone(); let mut member = all_members
.iter()
.find(|m| m.user_id() == user_id)
.unwrap()
.clone();
member.remove_roles(ctx.http.clone(), &roles[..]).unwrap(); member.remove_roles(ctx.http.clone(), &roles[..]).unwrap();
} }
} }
info!(" Sync: (any) superflous roles removed");
info!("Role reaction sync complete");
} }
fn get_all_role_reaction_message( fn get_all_role_reaction_message(
...@@ -98,7 +181,9 @@ fn get_all_role_reaction_message( ...@@ -98,7 +181,9 @@ fn get_all_role_reaction_message(
&'static HashMap<String, serenity::model::id::RoleId>, &'static HashMap<String, serenity::model::id::RoleId>,
)> { )> {
let guild = ctx.http.get_guild(CONFIG.server_id).unwrap(); let guild = ctx.http.get_guild(CONFIG.server_id).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()).unwrap();
info!(" Find role-react message: channels determined");
channels channels
.iter() .iter()
.flat_map(|channel| { .flat_map(|channel| {
......
...@@ -42,14 +42,6 @@ pub fn new_member(ctx: &Context, mut new_member: Member) { ...@@ -42,14 +42,6 @@ pub fn new_member(ctx: &Context, mut new_member: Member) {
pub struct Commands; pub struct Commands;
impl Commands { impl Commands {
pub fn join(ctx: Context, msg: Message, _content: &str) {
e!(
"Unable to get user: {:?}",
serenity::model::id::GuildId(CONFIG.server_id)
.member(ctx.http.clone(), msg.author.id)
.map(|member| new_member(&ctx, member))
);
}
pub fn register(ctx: Context, msg: Message, account_name: &str) { pub fn register(ctx: Context, msg: Message, account_name: &str) {
if account_name.is_empty() { if account_name.is_empty() {
e!( e!(
......
use serenity::model::{channel::ReactionType, guild::PartialGuild}; use serenity::model::{channel::ReactionType, guild::PartialGuild};
pub fn get_string_from_react(react: ReactionType) -> String { pub fn get_string_from_react(react: &ReactionType) -> String {
match react { match react {
ReactionType::Custom { ReactionType::Custom {
name: Some(name), .. name: Some(name), ..
} => name, } => name.to_string(),
ReactionType::Custom { id, name: None, .. } => id.to_string(), ReactionType::Custom { id, name: None, .. } => id.to_string(),
ReactionType::Unicode(name) => name, ReactionType::Unicode(name) => name.to_string(),
_ => format!("Unrecognised reaction type: {:?}", react), _ => format!("Unrecognised reaction type: {:?}", react),
} }
} }
...@@ -18,6 +18,6 @@ pub fn get_react_from_string(string: String, guild: PartialGuild) -> ReactionTyp ...@@ -18,6 +18,6 @@ pub fn get_react_from_string(string: String, guild: PartialGuild) -> ReactionTyp
.find(|e| e.name == string) .find(|e| e.name == string)
.map_or_else( .map_or_else(
|| ReactionType::from(string), // unicode emoji || ReactionType::from(string), // unicode emoji
|custom_emoji| ReactionType::from(custom_emoji.id), |custom_emoji| ReactionType::from(custom_emoji.clone()),
) )
} }
...@@ -56,16 +56,23 @@ impl Commands { ...@@ -56,16 +56,23 @@ impl Commands {
); );
} }
pub fn cowsay(ctx: Context, msg: Message, content: &str) { pub fn cowsay(ctx: Context, msg: Message, content: &str) {
let mut text = content.to_owned(); let output = if !content.trim().is_empty() {
text.escape_default(); let mut text = content.to_owned();
// Guess what buddy! You definitely are passing a string to cowsay text.escape_default();
text.insert(0, '\''); // Guess what buddy! You definitely are passing a string to cowsay
text.insert(text.len(), '\''); text.insert(0, '\'');
let output = std::process::Command::new("cowsay") text.insert(text.len(), '\'');
.arg(text) std::process::Command::new("cowsay")
.output() .arg(text)
// btw, if we can't execute cowsay we crash .output()
.expect("failed to execute cowsay"); .expect("failed to execute cowsay")
} else {
std::process::Command::new("sh")
.arg("-c")
.arg("sh -c fortune | cowsay -f \"/usr/share/cowsay/cows/$(echo 'www\nhellokitty\nbud-frogs\nkoala\nsuse\nthree-eyes\npony-smaller\nsheep\nvader\ncower\nmoofasa\nelephant\nflaming-sheep\nskeleton\nsnowman\ntux\napt\nmoose' | shuf -n 1).cow\"")
.output()
.expect("failed to execute fortune/cowsay")
};
let mut message = MessageBuilder::new(); let mut message = MessageBuilder::new();
message.push_codeblock( message.push_codeblock(
String::from_utf8(output.stdout).expect("unable to parse stdout to String"), String::from_utf8(output.stdout).expect("unable to parse stdout to String"),
...@@ -179,17 +186,17 @@ fn get_cached_motion(ctx: &Context, msg: &Message) -> MotionInfo { ...@@ -179,17 +186,17 @@ fn get_cached_motion(ctx: &Context, msg: &Message) -> MotionInfo {
let mut m = HashMap::new(); let mut m = HashMap::new();
m.insert( m.insert(
CONFIG.for_vote.to_string(), CONFIG.for_vote.to_string(),
msg.reaction_users(ctx, CONFIG.for_vote.to_string(), None, None) msg.reaction_users(ctx, CONFIG.for_vote.to_string(), Some(100), None)
.unwrap(), .unwrap(),
); );
m.insert( m.insert(
CONFIG.against_vote.to_string(), CONFIG.against_vote.to_string(),
msg.reaction_users(ctx, CONFIG.against_vote.to_string(), None, None) msg.reaction_users(ctx, CONFIG.against_vote.to_string(), Some(100), None)
.unwrap(), .unwrap(),
); );
m.insert( m.insert(
CONFIG.abstain_vote.to_string(), CONFIG.abstain_vote.to_string(),
msg.reaction_users(ctx, CONFIG.abstain_vote.to_string(), None, None) msg.reaction_users(ctx, CONFIG.abstain_vote.to_string(), Some(100), None)
.unwrap(), .unwrap(),
); );
m m
...@@ -253,7 +260,7 @@ fn update_motion( ...@@ -253,7 +260,7 @@ fn update_motion(
" {:10} {:6} {} on {}", " {:10} {:6} {} on {}",
user.name, user.name,
change, change,
get_string_from_react(reaction.emoji), get_string_from_react(&reaction.emoji),
topic topic
); );
...@@ -339,7 +346,7 @@ fn update_motion( ...@@ -339,7 +346,7 @@ fn update_motion(
} }
pub fn reaction_add(ctx: Context, add_reaction: channel::Reaction) { pub fn reaction_add(ctx: Context, add_reaction: channel::Reaction) {
let react_as_string = get_string_from_react(add_reaction.emoji.clone()); let react_as_string = get_string_from_react(&add_reaction.emoji);
match add_reaction.message(&ctx.http) { match add_reaction.message(&ctx.http) {
Ok(mut message) => { Ok(mut message) => {
if let Ok(user) = add_reaction.user(&ctx) { if let Ok(user) = add_reaction.user(&ctx) {
...@@ -414,7 +421,7 @@ pub fn reaction_remove(ctx: Context, removed_reaction: channel::Reaction) { ...@@ -414,7 +421,7 @@ pub fn reaction_remove(ctx: Context, removed_reaction: channel::Reaction) {
let mut motion_info = get_cached_motion(&ctx, &message); let mut motion_info = get_cached_motion(&ctx, &message);
if let Some(vote) = motion_info if let Some(vote) = motion_info
.votes .votes
.get_mut(&get_string_from_react(removed_reaction.emoji.clone())) .get_mut(&get_string_from_react(&removed_reaction.emoji))
{ {
vote.retain(|u| u.id != user.id); vote.retain(|u| u.id != user.id);
} }
......