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