Compare commits
3 Commits
Author | SHA1 | Date |
---|---|---|
|
69e317487d | |
|
d3469fb9fb | |
|
6a745cfd54 |
|
@ -15,3 +15,4 @@ credentials.ini
|
|||
config.ini
|
||||
list.txt
|
||||
members_database/
|
||||
spam_tracking_database/
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
36
README.md
36
README.md
|
@ -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
|
||||
|
|
125
src/bot.rs
125
src/bot.rs
|
@ -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;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"));
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
|
|
Loading…
Reference in New Issue