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
list.txt
members_database/
spam_tracking_database/

View File

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

View File

@ -2,8 +2,6 @@
FROM debian:stable-slim
ARG package
ADD $package ./
COPY list.txt .
RUN chmod +x matrix-modbot
RUN touch 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?
**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`
Without root:
@ -21,22 +21,39 @@ cargo build --release
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
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
- Easy to configure
- Configurable swear detection
- Spam detection (WIP)
- Configurable swear detection based on list of blocked words
- Spam detection
- Anti-Caps
- Anti-ASCII Art Spam
- Reputation system
- 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
- 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
For awarding someone reputation:
@ -47,3 +64,8 @@ For deducting someone's reputation (moderators only):
To get own 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!(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 {
let mut counter: f32 = 0.0;
if message.len() >= 20{
if message.len() >= 20 {
for char in message.chars() {
if char.is_ascii_whitespace() || char.is_ascii_punctuation() {
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) {
@ -267,15 +269,16 @@ pub mod bot {
let member_id: &str = member.user_id().as_str();
if member.power_level() <= 50 {
// Won't kick mods and admins
if let Ok(_) = self
if (self
.joined_room
.kick_user(member.user_id(), Some(reason))
.await
.await)
.is_ok()
{
dbg!(self.database_handle.remove(member_id).unwrap());
self.send_message(&format!("Member {} has been kicked.", member_id))
.await;
};
}
} else {
self.send_message("Cannot kick moderators and admins").await;
}
@ -288,53 +291,65 @@ pub mod bot {
}
async fn detect_spam(&mut self, event: &OriginalSyncRoomMessageEvent) {
let author = self
.joined_room
.get_member(&event.sender)
.await
.unwrap()
.unwrap();
let author_name = author.user_id().as_str().to_string();
let curr_utc = Utc::now().timestamp();
let expire_time: i64 = curr_utc - 5;
let mut expired_msgs: Vec<i64> = vec![];
if let Some(author) = self.joined_room.get_member(&event.sender).await.unwrap() {
let author_name = author.user_id().as_str().to_string();
let curr_utc = Utc::now().timestamp();
let expire_time: i64 = curr_utc - 5;
let mut expired_msgs: Vec<i64> = vec![];
let spam_data = self.spam_db_handle.get(&author_name);
match spam_data {
Ok(_) => {
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());
if !data_vec.is_empty() {
for time in &data_vec {
if time < &expire_time {
expired_msgs.push(*time)
let spam_data = self.spam_db_handle.get(&author_name);
match spam_data {
Ok(_) => {
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(),
);
if !data_vec.is_empty() {
for time in &data_vec {
if time < &expire_time {
expired_msgs.push(*time)
}
}
for msg in expired_msgs {
let _ = &data_vec.retain(|value| *value != msg);
}
}
for msg in expired_msgs {
let _ = &data_vec.retain(|value| *value != msg);
data_vec.push(curr_utc);
if data_vec.len() > 3 && author_name != self.info.user_id {
self.delete_message_from_room(&event.event_id, "Spamming")
.await;
self.update_reputation_for_member(&author, -1)
.await
.unwrap();
}
dbg!(self
.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());
}
data_vec.push(curr_utc);
if data_vec.len() > 5 && author_name != self.info.user_id {
self.delete_message_from_room(&event.event_id, "Spamming")
.await;
self.update_reputation_for_member(&author, -1)
.await
.unwrap();
}
dbg!(self.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());
Err(_) => {
dbg!(self
.spam_db_handle
.insert(&author_name, "[]".as_bytes())
.unwrap());
}
},
Err(_) => {
dbg!(self.spam_db_handle.insert(&author_name, "[]".as_bytes()).unwrap());
}
} else {
self.send_message("Problem getting author of message").await;
}
}
@ -414,17 +429,17 @@ pub mod bot {
.await
.unwrap()
.unwrap();
let user_data = dbg!(self
.database_handle
.get(author.user_id().as_str())
.unwrap()
.unwrap());
let (_, reputation) = convert_from_bytes_sled(&user_data);
self.send_message(
format!("Your current reputation is: {}", reputation).as_str(),
)
.await;
if let Some(user_data) =
dbg!(self.database_handle.get(author.user_id().as_str()).unwrap())
{
let (_, reputation) = convert_from_bytes_sled(&user_data);
self.send_message(
format!("Your current reputation is: {}", reputation).as_str(),
)
.await;
} else {
self.send_message("Error getting reputation").await;
}
};
}
}

View File

@ -97,8 +97,7 @@ pub mod utils {
.split(',')
.map(|n| n.trim().parse().unwrap())
.collect()
}
else {
} else {
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 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 IN CAPS"));
assert!(!detect_swear_from_message(
&swear_list,
"This isn't a swear"