Skip to content

Commit 4835012

Browse files
authored
Merge pull request #45 from rust-lang/temp-ban
Temporary ban with automated unban
2 parents 3dc8a62 + 1bc7e08 commit 4835012

File tree

9 files changed

+201
-33
lines changed

9 files changed

+201
-33
lines changed

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ license = "MIT"
88
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
99

1010
[dependencies]
11-
serenity = { version = "0.8.0", features = ["cache", "model"] }
11+
serenity = { version = "0.8.0", features = ["model"] }
1212
diesel = { version = "1.4.0", features = ["postgres", "r2d2"] }
1313
diesel_migrations = { version = "1.4.0", features = ["postgres"] }
1414
reqwest = { version = "0.10", features = ["blocking", "json"] }
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
-- This file should undo anything in `up.sql`
2+
DROP TABLE IF EXISTS bans;
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
-- Your SQL goes here
2+
CREATE TABLE IF NOT EXISTS bans (
3+
id SERIAL PRIMARY KEY,
4+
user_id TEXT NOT NULL,
5+
guild_id TEXT NOT NULL,
6+
unbanned BOOLEAN NOT NULL DEFAULT false,
7+
start_time TIMESTAMP NOT NULL,
8+
end_time TIMESTAMP NOT NULL
9+
);

src/api.rs

Lines changed: 5 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1-
use crate::commands::{Args, Result};
2-
use crate::db::DB;
3-
use crate::schema::roles;
1+
use crate::{
2+
commands::{Args, Result},
3+
db::DB,
4+
schema::roles,
5+
};
46
use diesel::prelude::*;
57
use serenity::{model::prelude::*, utils::parse_username};
68

@@ -93,24 +95,3 @@ pub(crate) fn kick(args: Args) -> Result<()> {
9395
}
9496
Ok(())
9597
}
96-
97-
/// Ban an user from the guild.
98-
///
99-
/// Requires the ban members permission
100-
pub(crate) fn ban(args: Args) -> Result<()> {
101-
if is_mod(&args)? {
102-
let user_id = parse_username(
103-
&args
104-
.params
105-
.get("user")
106-
.ok_or("unable to retrieve user param")?,
107-
)
108-
.ok_or("unable to retrieve user id")?;
109-
110-
if let Some(guild) = args.msg.guild(&args.cx) {
111-
info!("Banning user from guild");
112-
guild.read().ban(args.cx, UserId::from(user_id), &"all")?
113-
}
114-
}
115-
Ok(())
116-
}

src/ban.rs

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
use crate::{
2+
api,
3+
commands::{Args, Result},
4+
db::DB,
5+
schema::bans,
6+
text::ban_message,
7+
};
8+
use diesel::prelude::*;
9+
use serenity::{model::prelude::*, prelude::*, utils::parse_username};
10+
use std::{
11+
sync::atomic::{AtomicBool, Ordering},
12+
thread::sleep,
13+
time::{Duration, SystemTime},
14+
};
15+
16+
const HOUR: u64 = 3600;
17+
static UNBAN_THREAD_INITIALIZED: AtomicBool = AtomicBool::new(false);
18+
19+
pub(crate) fn save_ban(user_id: String, guild_id: String, hours: u64) -> Result<()> {
20+
info!("Recording ban for user {}", &user_id);
21+
let conn = DB.get()?;
22+
diesel::insert_into(bans::table)
23+
.values((
24+
bans::user_id.eq(user_id),
25+
bans::guild_id.eq(guild_id),
26+
bans::start_time.eq(SystemTime::now()),
27+
bans::end_time.eq(SystemTime::now()
28+
.checked_add(Duration::new(hours * HOUR, 0))
29+
.ok_or("out of range Duration for ban end_time")?),
30+
))
31+
.execute(&conn)?;
32+
33+
Ok(())
34+
}
35+
36+
pub(crate) fn save_unban(user_id: String, guild_id: String) -> Result<()> {
37+
info!("Recording unban for user {}", &user_id);
38+
let conn = DB.get()?;
39+
diesel::update(bans::table)
40+
.filter(
41+
bans::user_id
42+
.eq(user_id)
43+
.and(bans::guild_id.eq(guild_id).and(bans::unbanned.eq(false))),
44+
)
45+
.set(bans::unbanned.eq(true))
46+
.execute(&conn)?;
47+
48+
Ok(())
49+
}
50+
51+
pub(crate) fn start_unban_thread(cx: Context) {
52+
use std::str::FromStr;
53+
if !UNBAN_THREAD_INITIALIZED.load(Ordering::SeqCst) {
54+
UNBAN_THREAD_INITIALIZED.store(true, Ordering::SeqCst);
55+
type SendSyncError = Box<dyn std::error::Error + Send + Sync>;
56+
std::thread::spawn(move || -> std::result::Result<(), SendSyncError> {
57+
loop {
58+
let conn = DB.get()?;
59+
let to_unban = bans::table
60+
.filter(
61+
bans::unbanned
62+
.eq(false)
63+
.and(bans::end_time.le(SystemTime::now())),
64+
)
65+
.load::<(i32, String, String, bool, SystemTime, SystemTime)>(&conn)?;
66+
67+
for row in &to_unban {
68+
let guild_id = GuildId::from(u64::from_str(&row.2)?);
69+
info!("Unbanning user {}", &row.1);
70+
guild_id.unban(&cx, u64::from_str(&row.1)?)?;
71+
}
72+
sleep(Duration::new(HOUR, 0));
73+
}
74+
});
75+
}
76+
}
77+
78+
/// Temporarily ban an user from the guild.
79+
///
80+
/// Requires the ban members permission
81+
pub(crate) fn temp_ban(args: Args) -> Result<()> {
82+
if api::is_mod(&args)? {
83+
let user_id = parse_username(
84+
&args
85+
.params
86+
.get("user")
87+
.ok_or("unable to retrieve user param")?,
88+
)
89+
.ok_or("unable to retrieve user id")?;
90+
91+
use std::str::FromStr;
92+
93+
let hours = u64::from_str(
94+
args.params
95+
.get("hours")
96+
.ok_or("unable to retrieve hours param")?,
97+
)?;
98+
99+
let reason = args
100+
.params
101+
.get("reason")
102+
.ok_or("unable to retrieve reason param")?;
103+
104+
if let Some(guild) = args.msg.guild(&args.cx) {
105+
info!("Banning user from guild");
106+
let user = UserId::from(user_id);
107+
108+
user.create_dm_channel(args.cx)?
109+
.say(args.cx, ban_message(reason, hours))?;
110+
111+
guild.read().ban(args.cx, &user, &"all")?;
112+
113+
save_ban(
114+
format!("{}", user_id),
115+
format!("{}", guild.read().id),
116+
hours,
117+
)?;
118+
}
119+
}
120+
Ok(())
121+
}
122+
123+
pub(crate) fn help(args: Args) -> Result<()> {
124+
let hours = 24;
125+
let reason = "violating the code of conduct";
126+
127+
let help_string = format!(
128+
"
129+
Ban a user for a temporary amount of time
130+
```
131+
{command}
132+
```
133+
Example:
134+
```
135+
?ban @someuser {hours} {reason}
136+
```
137+
will ban a user for {hours} hours and send them the following message:
138+
```
139+
{user_message}
140+
```
141+
",
142+
command = "?ban {user} {hours} reason...",
143+
user_message = ban_message(reason, hours),
144+
hours = hours,
145+
reason = reason,
146+
);
147+
148+
api::send_reply(&args, &help_string)?;
149+
Ok(())
150+
}

src/main.rs

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,14 @@ extern crate diesel_migrations;
88
extern crate log;
99

1010
mod api;
11+
mod ban;
1112
mod commands;
1213
mod crates;
1314
mod db;
1415
mod schema;
1516
mod state_machine;
1617
mod tags;
18+
mod text;
1719
mod welcome;
1820

1921
use crate::db::DB;
@@ -111,7 +113,8 @@ fn app() -> Result {
111113
cmds.add("?kick {user}", api::kick);
112114

113115
// Ban
114-
cmds.add("?ban {user}", api::ban);
116+
cmds.add("?ban help", ban::help);
117+
cmds.add("?ban {user} {hours} reason...", ban::temp_ban);
115118

116119
// Post the welcome message to the welcome channel.
117120
cmds.add("?CoC {channel}", welcome::post_message);
@@ -150,7 +153,14 @@ impl RawEventHandler for Events {
150153
match event {
151154
Event::ReactionAdd(ref ev) => {
152155
if let Err(e) = welcome::assign_talk_role(&cx, ev) {
153-
println!("{}", e);
156+
error!("{}", e);
157+
}
158+
}
159+
Event::GuildBanRemove(ref ev) => {
160+
if let Err(e) =
161+
ban::save_unban(format!("{}", ev.user.id), format!("{}", ev.guild_id))
162+
{
163+
error!("{}", e);
154164
}
155165
}
156166
_ => (),
@@ -167,7 +177,8 @@ impl EventHandler for Messages {
167177
self.cmds.execute(cx, msg);
168178
}
169179

170-
fn ready(&self, _: Context, ready: Ready) {
180+
fn ready(&self, context: Context, ready: Ready) {
171181
info!("{} connected to discord", ready.user.name);
182+
ban::start_unban_thread(context);
172183
}
173184
}

src/schema.rs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,14 @@
1+
table! {
2+
bans (id) {
3+
id -> Int4,
4+
user_id -> Text,
5+
guild_id -> Text,
6+
unbanned -> Bool,
7+
start_time -> Timestamp,
8+
end_time -> Timestamp,
9+
}
10+
}
11+
112
table! {
213
messages (id) {
314
id -> Int4,
@@ -31,4 +42,4 @@ table! {
3142
}
3243
}
3344

34-
allow_tables_to_appear_in_same_query!(messages, roles, tags, users,);
45+
allow_tables_to_appear_in_same_query!(bans, messages, roles, tags, users,);

src/text.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
pub(crate) const WELCOME_BILLBOARD: &'static str = "By participating in this community, you agree to follow the Rust Code of Conduct, as linked below. Please click the :white_check_mark: below to acknowledge and gain access to the channels.
2+
3+
https://www.rust-lang.org/policies/code-of-conduct ";
4+
5+
pub(crate) fn ban_message(reason: &str, hours: u64) -> String {
6+
format!("You have been banned from The Rust Programming Language discord server for {}. The ban will expire in {} hours. If you feel this action was taken unfairly, you can reach the Rust moderation team at [email protected]", reason, hours)
7+
}

src/welcome.rs

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ use crate::{
33
commands::Args,
44
db::DB,
55
schema::{messages, roles, users},
6+
text::WELCOME_BILLBOARD,
67
Result,
78
};
89
use diesel::prelude::*;
@@ -12,10 +13,6 @@ use serenity::{model::prelude::*, prelude::*};
1213
pub(crate) fn post_message(args: Args) -> Result {
1314
use std::str::FromStr;
1415

15-
const WELCOME_BILLBOARD: &'static str = "By participating in this community, you agree to follow the Rust Code of Conduct, as linked below. Please click the :white_check_mark: below to acknowledge and gain access to the channels.
16-
17-
https://www.rust-lang.org/policies/code-of-conduct ";
18-
1916
if api::is_mod(&args)? {
2017
let channel_name = &args
2118
.params

0 commit comments

Comments
 (0)