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 (114)
*.db filter=lfs diff=lfs merge=lfs -text
/target/
Cargo.lock
**/*.rs.bk
src/discord_token
ucc-bot.log
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": ""
}
]
}
# Packages required to build
On ubuntu (such as using ubuntu with WSL) you will need these packages to build:
* libsqlite3-dev
* pkg-config
* libssl-dev
This diff is collapsed.
......@@ -5,10 +5,25 @@ authors = ["tec <tec@ucc.gu.uwa.edu.au>"]
edition = "2018"
[dependencies]
serenity = "^0.7.1"
rand = "^0.7.2"
lazy_static = "^1.4.0"
log = "^0.4.8"
simplelog = "^0.7.4"
toml = "^0.5.6"
serde = "^1.0.104"
aes = "0.6"
async-trait = "0.1.42"
base64 = "0.13.0"
block-modes = "0.7"
chrono = "0.4.19"
diesel = { version = "1.4.5", features = ["sqlite"] }
guard = "0.5.0"
ical = "0.7.0"
indexmap = { version = "1.6.1", features = ["serde-1"] }
lazy_static = "1.4.0"
ldap3 = "0.9.1"
log = "0.4.11"
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"
#+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
# TODO
## Reaction Roles
_Involves: File r/w + parsing, discord reactions_
So, for reaction roles, afaict this is what needs to be done
- Migrate config.rs to something like `config.toml`
- Complete `reaction_roles.rs`
- Load from config (roles, and the rr msg, if they exist)
- Code up the `!rr init` command
- Monitor reactions, update user roles etc.
- On updated to `config.toml` (and on bot load, now that I think of it) overwrite the rr msg with content based on roles in config
- **Bonus:** add command like `!rr add :emoji: role name`
## LDAP Integration
_Involves: LDAP r/w, email,discord nicknames+roles_
- Add a `discord_ID` or similar field to the LDAP database
- Add a `!link <ucc-username>` command, which
- Created a verification token
- Perhaps just hash discordID+username with hardcoded salt (is this bad?)
- Sends an email with a verification token (and instruction to `!register`)
- Goes back to discord and run `!register <token>`
- Add a `!register <token>` command
- Do hash, compare, if same update LDAP database
- Either add `registered` role or remove `unregistered`
- End result, unable to change nickname
- Set nickname to something like `<tla> (<first-name>)`
- Add a `!unlink` command
- Remove LDAP entry
- Make able to change nickname again
- **Bonus:** be able to specify nickname format for registered users in `config.toml`, and add way to update
## Fun with accounts
- `!dispense <item name>`
- Dispense the item
#+TITLE: TODO
* Reaction Roles
_Involves: File r/w + parsing, discord reactions_
So, for reaction roles, afaict this is what needs to be done
- [X] Migrate config.rs to something like ~config.yml~
- [X] Complete ~reaction_roles.rs~
- [X] Load from config (roles, and the rr msg, if they exist)
- [X] Monitor reactions, update user roles etc.
- [X] Only allow reacts which correspond to roles
* LDAP Integration
_Involves: LDAP r/w, email,discord nicknames+roles_
- [ ] Add a ~discord_ID~ or similar field to the LDAP database
- [ ] Add a ~!link <ucc-username>~ command, which
- [ ] Created a verification token
- [ ] Perhaps just hash discordID+username with hardcoded salt (is this bad?)
- [ ] Sends an email with a verification token (and instruction to ~!register~)
- [ ] Goes back to discord and run ~!register <token>~
- [ ] Add a ~!register <token>~ command
- [ ] Do hash, compare, if same update LDAP database
- [ ] Either add ~registered~ role or remove ~unregistered~
- [ ] End result, unable to change nickname
- [ ] Set nickname to something like ~<tla> (<first-name>)~
- [ ] Add a ~!unlink~ command
- [ ] Remove LDAP entry
- [ ] Make able to change nickname again
- [ ] *Bonus:* be able to specify nickname format for registered users in ~config.toml~, and add way to update
* Fun with accounts
- ~!dispense <item name>~
- Dispense the item
File added
use serenity;
use std::fs;
use serde::Deserialize;
use toml;
lazy_static! {
static ref CONFIG_FILE: String = fs::read_to_string("config.toml").unwrap();
}
lazy_static! {
pub static ref CONFIG: UccbotConfig = toml::from_str(&CONFIG_FILE).unwrap();
}
pub static DISCORD_TOKEN: &str = include_str!("discord_token");
#[derive(Deserialize)]
pub struct UccbotConfig {
pub server_id: u64,
// #general
pub main_channel: serenity::model::id::ChannelId,
// #the-corner
pub welcome_channel: serenity::model::id::ChannelId,
// #general
pub announcement_channel: serenity::model::id::ChannelId,
pub bot_id: u64,
pub vote_pool_size: i8,
pub vote_role: u64,
pub tiebreaker_role: u64,
pub unregistered_member_role: u64,
pub registered_member_role: u64,
pub command_prefix: &'static str,
pub for_vote: &'static str,
pub against_vote: &'static str,
pub abstain_vote: &'static str,
pub approve_react: &'static str,
pub disapprove_react: &'static str,
pub unsure_react: &'static str,
}
impl UccbotConfig {
pub fn allowed_reacts(&self) -> Vec<String> {
vec!(self.for_vote.to_string(),
self.against_vote.to_string(),
self.abstain_vote.to_string(),
self.approve_react.to_string(),
self.disapprove_react.to_string(),
self.unsure_react.to_string())
}
}
\ No newline at end of file
use indexmap::IndexMap;
use serde::Deserialize;
use serde_yaml;
use serenity::model::id;
use std::fs;
lazy_static! {
static ref CONFIG_FILE: String = fs::read_to_string("config.yml").unwrap();
pub static ref CONFIG: UccbotConfig = serde_yaml::from_str(&CONFIG_FILE).unwrap();
static ref SECRETS_FILE: String = fs::read_to_string("secrets.yml").unwrap();
pub static ref SECRETS: UccbotSecrets = serde_yaml::from_str(&SECRETS_FILE).unwrap();
}
#[derive(Debug, Deserialize)]
pub struct UccbotConfig {
pub server_id: u64,
pub main_channel: id::ChannelId,
pub welcome_channel: id::ChannelId,
pub announcement_channel: id::ChannelId,
pub readme_channel: id::ChannelId,
pub bot_id: id::UserId,
pub ical_url: Option<String>,
pub vote_pool_size: i8,
pub vote_role: u64,
pub tiebreaker_role: u64,
pub unregistered_member_role: u64,
pub registered_member_role: u64,
pub expired_member_role: u64,
pub command_prefix: String,
pub for_vote: String,
pub against_vote: String,
pub abstain_vote: String,
pub approve_react: String,
pub disapprove_react: String,
pub unsure_react: String,
pub react_role_messages: Vec<ReactionMapping>,
#[serde(default = "ldap_bind_address")]
pub bind_address: String,
}
pub fn ldap_bind_address() -> String {
"ldaps://samson.ucc.asn.au:636".to_string()
}
impl UccbotConfig {
pub fn allowed_reacts(&self) -> Vec<String> {
vec![
self.for_vote.to_string(),
self.against_vote.to_string(),
self.abstain_vote.to_string(),
self.approve_react.to_string(),
self.disapprove_react.to_string(),
self.unsure_react.to_string(),
]
}
}
#[derive(Debug, Deserialize)]
pub struct UccbotSecrets {
pub ldap_pass: String,
pub discord_token: String,
}
pub type ReactRoleMap = IndexMap<String, id::RoleId>;
#[derive(Debug, Deserialize, Clone)]
pub struct ReactionMapping {
pub message: serenity::model::id::MessageId,
#[serde(default = "empty_rr_map")]
pub mapping: ReactRoleMap,
}
fn empty_rr_map() -> ReactRoleMap {
IndexMap::new()
}
server_id = 606351521117896704
#general
main_channel = 606351521117896706
#the-corner
welcome_channel = 606351613816209418
#general
announcement_channel = 606351521117896706
bot_id = 607078903969742848
vote_pool_size = 2
vote_role = 607478818038480937
tiebreaker_role = 607509283483025409
unregistered_member_role = 608282247350714408
registered_member_role = 608282133118582815
command_prefix = "!"
for_vote = "👍"
against_vote = "👎"
abstain_vote = "🙊"
approve_react = "⬆"
disapprove_react = "⬇"
unsure_react = "❔"
server_id: 606351521117896704 # general
main_channel: 606351521117896706 # the-corner
welcome_channel: 606351613816209418 # general
announcement_channel: 606351521117896706 # the-corner
readme_channel: 606351613816209418 # general
bot_id: 607078903969742848
vote_pool_size: 2
vote_role: 607478818038480937 # Vote Role
tiebreaker_role: 607509283483025409 # tie-breaker
unregistered_member_role: 608282247350714408 # unregistered
registered_member_role: 608282133118582815 # registered
expired_member_role: 607479030370926613 # registered
command_prefix: "!"
for_vote: "👍"
against_vote: "👎"
abstain_vote: "🙊"
approve_react: "⬆"
disapprove_react: "⬇"
unsure_react: "❔"
react_role_messages:
- message: 674164298632790026
mapping:
🐊: 609708723006472198 # Autonomous Alligators
🐃: 609708839243087892 # Bipedal Bison
🦆: 609708889763479562 # Omnipresent Ostriches
j5: 607478818038480937 # Vote role
👻: 634415546804338688 # Background charachter
server_id = 606351521117896704
#general
main_channel = 606351521117896706
#the-corner
welcome_channel = 606351613816209418
#general
announcement_channel = 606351521117896706
bot_id = 607078903969742848
vote_pool_size = 2
vote_role = 607478818038480937
tiebreaker_role = 607509283483025409
unregistered_member_role = 608282247350714408
registered_member_role = 608282133118582815
command_prefix = "!"
for_vote = "👍"
against_vote = "👎"
abstain_vote = "🙊"
approve_react = "⬆"
disapprove_react = "⬇"
unsure_react = "❔"
server_id = 264401248676085760
#ucc
main_channel = 264401248676085760
#welcome
welcome_channel = 606750983699300372
#committee
announcement_channel = 264411219627212801
bot_id = 635407267881156618
vote_pool_size = 7
vote_role = 269817189966544896
tiebreaker_role = 635370432568098817
unregistered_member_role = 0 # does not exist
registered_member_role = 0 # does not exist
command_prefix = "!"
for_vote = "👍"
against_vote = "👎"
abstain_vote = "🙊"
approve_react = "⬆"
disapprove_react = "⬇"
unsure_react = "❔"
server_id: 264401248676085760 # ucc
main_channel: 264401248676085760 # ucc
welcome_channel: 606750983699300372 # welcome
announcement_channel: 264411219627212801 # committee
readme_channel: 674252245008908298 # readme
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_role: 269817189966544896 # @committee
tiebreaker_role: 0 # No tiebreak apparently 635370432568098817 # @Presiding Presidenterino
unregistered_member_role: 674641042464833548 # @unregistered
registered_member_role: 692754285557055490 # @member
expired_member_role: 692754285557055490 # @member
command_prefix: "!"
for_vote: "👍"
against_vote: "👎"
abstain_vote: "🙊"
approve_react: "⬆"
disapprove_react: "⬇"
unsure_react: "❔"
react_role_messages:
- message: 674652973103579225 # Intro
- message: 674653014119809025 # Club-related
mapping:
uwa: 674254888221278226
🥤: 691852097003585577
cpu: 674255083063738368
🎮: 691852441091833896
🤖: 696241187035676763
📐: 696241431550885888
🧬: 696241817397624862
: 696241928492286044
🧮: 696242272844382209
💸: 696242320609378345
📚: 696257213068607518
🎨: 696242397407084614
: 696242439962230854
🌥️: 696242479527100506
: 696242521738838016
🌤: 696242572288589864
: 696242617301860403
- message: 674653030817464370 # Operating System
mapping:
win10: 674255085307691039
capple: 674255209916137492
debian: 674255247824125955
arch: 674255274957340673
fedora: 674260517858181122
linux: 674255299494019093
bsd: 674255522433859604
- message: 674653049578455060 # Editor
mapping:
vim: 674255633356423208
emacs: 674255739061010443
vscode: 674255746833186816
sublime: 674255747391029249
atom_: 674255748426891274
intellij: 674255748959567901
npp: 674255750297812993
eclipse: 696281046924394526
- message: 674653069526695946 # Programming (1)
mapping:
html: 674255751031685130
css: 674255751707099136
js: 674255752239775784
ts: 674255752860532795
php: 674255753321775134
python: 674255753787211778
R_: 674255754278207511
julia: 674255755305680901
matlab: 674255755809128448
mathematica: 674255756320833536
- message: 674653087075532810 # Programming (2)
mapping:
c_: 674256084403224673
cpp: 674256084826849281
csharp: 674256086261563442
java: 674256086748102666
rust: 674256087180115992
binary: 674256088199200774
haskell: 674256088861769738
ruby: 674256089742835712
scala: 674256090321387530
lua: 674256090795474944
go: 674256091441397761
tex: 674256092187983897
git: 674256093064593439
- message: 674653104666312704 # games
mapping:
et: 728997559061708922
factorio: 728997562710884373
minecraft: 728997565496033320
server_id: 606351521117896704
#general
main_channel: 606351521117896706
#the-corner
welcome_channel: 606351613816209418
#general
announcement_channel: 606351521117896706
bot_id: 607078903969742848
vote_pool_size: 2
vote_role: 607478818038480937
tiebreaker_role: 607509283483025409
unregistered_member_role: 608282247350714408
registered_member_role: 608282133118582815
expired_member_role: 0
command_prefix: "!"
for_vote: "👍"
against_vote: "👎"
abstain_vote: "🙊"
approve_react: "⬆"
disapprove_react: "⬇"
unsure_react: "❔"
use diesel::prelude::*;
use diesel::result::Error;
use diesel::sqlite::SqliteConnection;
// TODO reuse DB connection, using r2d2 or something
use crate::ldap::*;
#[table_name = "members"]
#[derive(Queryable, AsChangeset, Insertable)]
pub struct Member {
pub discord_id: i64,
pub tla: Option<String>,
pub username: String,
pub member_since: Option<String>,
pub name: Option<String>,
pub biography: Option<String>,
pub github: Option<String>,
pub photo: Option<String>,
pub website: Option<String>,
pub study: Option<String>,
}
table! {
members (discord_id) {
discord_id -> BigInt,
tla -> Nullable<Text>,
username -> Text,
member_since -> Nullable<Text>,
name -> Nullable<Text>,
biography -> Nullable<Text>,
github -> Nullable<Text>,
photo -> Nullable<Text>,
website -> Nullable<Text>,
study -> Nullable<Text>,
}
}
pub fn db_connection() -> SqliteConnection {
SqliteConnection::establish("state.db").expect("Failed to connect to sqlite DB")
}
pub fn add_member(discord_id: &u64, username: &str) -> Member {
let ldap_user = ldap_search(username);
let name = ldap_user.as_ref().map(|u| u.name.clone());
let tla_user = tla_search(username);
let tla = tla_user.as_ref().map(|u| u.tla.clone()).flatten();
let new_member = Member {
discord_id: *discord_id as i64,
username: username.to_string(),
name: name.clone(),
tla: tla,
member_since: None,
biography: None,
github: None,
photo: None,
website: None,
study: None,
};
diesel::insert_into(members::table)
.values(&new_member)
.execute(&db_connection())
.expect("Failed to add member to DB");
info!(
"{} added to member DB",
name.unwrap_or(discord_id.to_string())
);
new_member
}
#[allow(dead_code)] // remove this if you start using it
pub fn update_member(discord_id: &u64, member: Member) -> Result<usize, Error> {
diesel::update(members::table.find(*discord_id as i64))
.set(&member)
.execute(&db_connection())
}
pub fn username_exists(username: &str) -> bool {
get_member_info_from_username(username).is_ok()
}
pub fn get_member_info(discord_id: &u64) -> Result<Member, Error> {
members::table
.find(*discord_id as i64)
.first(&db_connection())
}
pub fn get_member_info_from_username(username: &str) -> Result<Member, Error> {
members::table
.filter(members::username.eq(username))
.first(&db_connection())
}
pub fn get_member_info_from_tla(tla: &str) -> Result<Member, Error> {
members::table
.filter(members::tla.eq(tla))
.first(&db_connection())
}
pub fn set_member_bio(discord_id: &u64, bio: Option<&str>) -> Result<usize, Error> {
diesel::update(members::table.find(*discord_id as i64))
.set(members::biography.eq(bio))
.execute(&db_connection())
}
pub fn set_member_git(discord_id: &u64, git: Option<&str>) -> Result<usize, Error> {
diesel::update(members::table.find(*discord_id as i64))
.set(members::github.eq(git))
.execute(&db_connection())
}
pub fn set_member_photo(discord_id: &u64, url: Option<&str>) -> Result<usize, Error> {
diesel::update(members::table.find(*discord_id as i64))
.set(members::photo.eq(url))
.execute(&db_connection())
}
pub fn set_member_website(discord_id: &u64, url: Option<&str>) -> Result<usize, Error> {
diesel::update(members::table.find(*discord_id as i64))
.set(members::website.eq(url))
.execute(&db_connection())
}
pub fn set_member_study(discord_id: &u64, study: Option<&str>) -> Result<usize, Error> {
diesel::update(members::table.find(*discord_id as i64))
.set(members::study.eq(study))
.execute(&db_connection())
}