Compare commits

...

3 Commits
v1.0 ... main

Author SHA1 Message Date
David 69e317487d Added error handling for getting reputation 2023-02-01 18:33:18 +01:00
David d3469fb9fb Minor improvements 2023-01-16 12:45:52 +01:00
David 6a745cfd54 Minor bugfixes 2023-01-12 14:49:47 +01:00
8 changed files with 115 additions and 68 deletions

1
.gitignore vendored
View File

@ -15,3 +15,4 @@ credentials.ini
config.ini config.ini
list.txt list.txt
members_database/ members_database/
spam_tracking_database/

View File

@ -16,7 +16,7 @@ run_tests_cargo:
script: script:
- rustc --version && cargo --version # Print version info for debugging - rustc --version && cargo --version # Print version info for debugging
- cargo build --release - cargo build --release
- cargo test --workspace --verbose - cargo test --release --workspace --verbose
artifacts: artifacts:
paths: paths:
- target/release/matrix-modbot - target/release/matrix-modbot

View File

@ -2,8 +2,6 @@
FROM debian:stable-slim FROM debian:stable-slim
ARG package ARG package
ADD $package ./ ADD $package ./
COPY list.txt .
RUN chmod +x matrix-modbot RUN chmod +x matrix-modbot
RUN touch config.ini RUN touch config.ini
CMD ["./matrix-modbot", "config.ini"] CMD ["./matrix-modbot", "config.ini"]
HEALTHCHECK --interval=30s --timeout=30s --start-period=30s --retries=3 CMD curl --fail http://localhost:5000/health || exit 1

View File

@ -9,7 +9,7 @@ The bot is orientated towards admins of large Matrix rooms that are public and r
## How to run? ## How to run?
**Using Docker:** **Using Docker:**
A minimal Docker image containing the compiled binary is published on [Docker Hub](https://hub.docker.com/r/davidlocalhost/matrix-modbot). Note that this image is automatically pushed using a CI/CD Pipeline for every commit made to the main branch. Expect there to be bugs and breaking changes. A minimal Docker image containing the compiled binary is published on [Docker Hub](https://hub.docker.com/r/davidlocalhost/matrix-modbot). Note that this image is automatically pushed using a CI/CD Pipeline for every commit made to the main branch. Expect there to be bugs and breaking changes. If you want to use a stable release, go to Deployments -> Releases and download the source code and build a Docker image containing a release binary.
`sudo docker run -v /local/path/to/config.ini:/config.ini -t matrix-modbot:latest` `sudo docker run -v /local/path/to/config.ini:/config.ini -t matrix-modbot:latest`
Without root: Without root:
@ -21,22 +21,39 @@ cargo build --release
cargo run /path/to/config.ini cargo run /path/to/config.ini
``` ```
Note that this bot will NOT run in an encrypted room. It also can only join 1 room at a time and isn't integrated with Element's Spaces feature. You must specify the room to join in the config file and invite the bot. You will also have to create an account for the bot and put the credentials into the config file. Note that this bot will NOT run in an encrypted room. It also can only join 1 room at a time and isn't integrated with Element's Spaces feature. You must specify the room to join in the config file and invite the bot. You will also have to create an account for the bot and put the credentials into the config file. It is possible to use this bot with Matrix-Discord bridge, as long as the bridge bot is set to have moderator privileges.
### Configuration ### Configuration
For configuration, see `example_config.ini`. The options should be self-explanatory. See example below
```
[credentials]
user_id = @bot:matrix.org
homeserver = matrix.org
password = insert-password-here
[room]
room = !insert-room-id:matrix.org
[swear_list]
url = https://raw.githubusercontent.com/chucknorris-io/swear-words/master/en
[rules]
allow_swear = false
```
Note that the configuration file should be specified if the file name is not `config.ini`.
## Features ## Features
- Easy to configure - Configurable swear detection based on list of blocked words
- Configurable swear detection - Spam detection
- Spam detection (WIP)
- Anti-Caps - Anti-Caps
- Anti-ASCII Art Spam
- Reputation system - Reputation system
- All members of a room have reputation points, they are deducted when spam/swear/caps are detected - All members of a room have reputation points, they are deducted when spam/swear/caps are detected
- Automatically kicks a member if reputation is below -15 - Automatically kicks a member if reputation is below -15
- Members can award each other with maximum 1 reputation point every 24hr - Members can award each other with maximum 1 reputation point every 24hr
- Users with power level >= 50 are not affected (Mods and Admins) - Users with power level >= 50 are not affected by the limit (Mods and Admins)
- Mods can warn specific members by deducting reputation points
### Commands ### Commands
For awarding someone reputation: For awarding someone reputation:
@ -47,3 +64,8 @@ For deducting someone's reputation (moderators only):
To get own reputation: To get own reputation:
- "!modbot reputation" - "!modbot reputation"
### Credits
Many thanks to:
- [brokenbyte](https://gitlab.com/brokenbyte) for all the help and support along the way
- [hebbot](https://github.com/haecker-felix/hebbot), as an example I could follow during the development of this bot

View File

@ -118,7 +118,9 @@ pub mod bot {
dbg!(db.insert(member.as_str(), &bytes).unwrap()); dbg!(db.insert(member.as_str(), &bytes).unwrap());
} }
{ {
dbg!(spam_db_handle.insert(member.as_str(), "[]".as_bytes()).unwrap()); dbg!(spam_db_handle
.insert(member.as_str(), "[]".as_bytes())
.unwrap());
} }
} }
} }
@ -213,14 +215,14 @@ pub mod bot {
fn detect_whitespace_spam(&self, message: &str) -> bool { fn detect_whitespace_spam(&self, message: &str) -> bool {
let mut counter: f32 = 0.0; let mut counter: f32 = 0.0;
if message.len() >= 20{ if message.len() >= 20 {
for char in message.chars() { for char in message.chars() {
if char.is_ascii_whitespace() || char.is_ascii_punctuation() { if char.is_ascii_whitespace() || char.is_ascii_punctuation() {
counter += 1.0 counter += 1.0
} }
} }
} }
counter/message.len() as f32 >= 0.8 counter / message.len() as f32 >= 0.8
} }
async fn delete_message_from_room(&self, event_id: &OwnedEventId, reason: &str) { async fn delete_message_from_room(&self, event_id: &OwnedEventId, reason: &str) {
@ -267,15 +269,16 @@ pub mod bot {
let member_id: &str = member.user_id().as_str(); let member_id: &str = member.user_id().as_str();
if member.power_level() <= 50 { if member.power_level() <= 50 {
// Won't kick mods and admins // Won't kick mods and admins
if let Ok(_) = self if (self
.joined_room .joined_room
.kick_user(member.user_id(), Some(reason)) .kick_user(member.user_id(), Some(reason))
.await .await)
.is_ok()
{ {
dbg!(self.database_handle.remove(member_id).unwrap()); dbg!(self.database_handle.remove(member_id).unwrap());
self.send_message(&format!("Member {} has been kicked.", member_id)) self.send_message(&format!("Member {} has been kicked.", member_id))
.await; .await;
}; }
} else { } else {
self.send_message("Cannot kick moderators and admins").await; self.send_message("Cannot kick moderators and admins").await;
} }
@ -288,12 +291,7 @@ pub mod bot {
} }
async fn detect_spam(&mut self, event: &OriginalSyncRoomMessageEvent) { async fn detect_spam(&mut self, event: &OriginalSyncRoomMessageEvent) {
let author = self if let Some(author) = self.joined_room.get_member(&event.sender).await.unwrap() {
.joined_room
.get_member(&event.sender)
.await
.unwrap()
.unwrap();
let author_name = author.user_id().as_str().to_string(); let author_name = author.user_id().as_str().to_string();
let curr_utc = Utc::now().timestamp(); let curr_utc = Utc::now().timestamp();
let expire_time: i64 = curr_utc - 5; let expire_time: i64 = curr_utc - 5;
@ -303,7 +301,11 @@ pub mod bot {
match spam_data { match spam_data {
Ok(_) => { Ok(_) => {
if spam_data.clone().unwrap().is_some() { if spam_data.clone().unwrap().is_some() {
let mut data_vec = convert_vec_to_str(str::from_utf8(&spam_data.unwrap().unwrap()[..]).unwrap().as_ref()); let mut data_vec = convert_vec_to_str(
str::from_utf8(&spam_data.unwrap().unwrap()[..])
.unwrap()
.as_ref(),
);
if !data_vec.is_empty() { if !data_vec.is_empty() {
for time in &data_vec { for time in &data_vec {
if time < &expire_time { if time < &expire_time {
@ -318,24 +320,37 @@ pub mod bot {
data_vec.push(curr_utc); data_vec.push(curr_utc);
if data_vec.len() > 5 && author_name != self.info.user_id { if data_vec.len() > 3 && author_name != self.info.user_id {
self.delete_message_from_room(&event.event_id, "Spamming") self.delete_message_from_room(&event.event_id, "Spamming")
.await; .await;
self.update_reputation_for_member(&author, -1) self.update_reputation_for_member(&author, -1)
.await .await
.unwrap(); .unwrap();
} }
dbg!(self
dbg!(self.spam_db_handle.insert(&author_name, format!("{:?}", data_vec).as_str().as_bytes()).unwrap()); .spam_db_handle
.insert(&author_name, format!("{:?}", data_vec).as_str().as_bytes())
.unwrap());
} else {
dbg!(self
.spam_db_handle
.insert(
&author_name,
format!("{:?}", vec![curr_utc]).as_str().as_bytes()
)
.unwrap());
} }
else {
dbg!(self.spam_db_handle.insert(&author_name, format!("{:?}", vec![curr_utc]).as_str().as_bytes()).unwrap());
} }
},
Err(_) => { Err(_) => {
dbg!(self.spam_db_handle.insert(&author_name, "[]".as_bytes()).unwrap()); dbg!(self
.spam_db_handle
.insert(&author_name, "[]".as_bytes())
.unwrap());
} }
} }
} else {
self.send_message("Problem getting author of message").await;
}
} }
async fn detect_command(&self, event: &OriginalSyncRoomMessageEvent, message: &str) { async fn detect_command(&self, event: &OriginalSyncRoomMessageEvent, message: &str) {
@ -414,17 +429,17 @@ pub mod bot {
.await .await
.unwrap() .unwrap()
.unwrap(); .unwrap();
let user_data = dbg!(self if let Some(user_data) =
.database_handle dbg!(self.database_handle.get(author.user_id().as_str()).unwrap())
.get(author.user_id().as_str()) {
.unwrap()
.unwrap());
let (_, reputation) = convert_from_bytes_sled(&user_data); let (_, reputation) = convert_from_bytes_sled(&user_data);
self.send_message( self.send_message(
format!("Your current reputation is: {}", reputation).as_str(), format!("Your current reputation is: {}", reputation).as_str(),
) )
.await; .await;
} else {
self.send_message("Error getting reputation").await;
}
}; };
} }
} }

View File

@ -97,8 +97,7 @@ pub mod utils {
.split(',') .split(',')
.map(|n| n.trim().parse().unwrap()) .map(|n| n.trim().parse().unwrap())
.collect() .collect()
} } else {
else {
vec![] vec![]
} }
} }
@ -113,6 +112,6 @@ pub mod utils {
} }
} }
counter / msg_length >= 0.8 (counter / msg_length) >= 0.65
} }
} }

View File

@ -0,0 +1,11 @@
#[cfg(test)]
mod tests {
use matrix_modbot::utils::utils::detect_caps;
#[test]
fn test_caps_detection() {
assert!(detect_caps("FULL CAPSSSSSS"));
assert!(!detect_caps("Not Full Caps But There Are Some Caps"));
assert!(detect_caps("CAPs BUT N0T FuLLY"));
}
}

View File

@ -8,6 +8,7 @@ mod tests {
let creds = BotUserInfo::get_info("tests/test_creds.ini").unwrap(); let creds = BotUserInfo::get_info("tests/test_creds.ini").unwrap();
let swear_list = create_swear_list(&creds.swear_list_url).await.unwrap(); let swear_list = create_swear_list(&creds.swear_list_url).await.unwrap();
assert!(detect_swear_from_message(&swear_list, "fuck you")); assert!(detect_swear_from_message(&swear_list, "fuck you"));
assert!(detect_swear_from_message(&swear_list, "FUCK YOU IN CAPS"));
assert!(!detect_swear_from_message( assert!(!detect_swear_from_message(
&swear_list, &swear_list,
"This isn't a swear" "This isn't a swear"