Coder Social home page Coder Social logo

finnbear / rustrict Goto Github PK

View Code? Open in Web Editor NEW
85.0 2.0 9.0 1.33 MB

rustrict is a profanity filter for Rust

Home Page: https://crates.io/crates/rustrict

License: MIT License

Rust 98.82% Makefile 0.78% HTML 0.40%
rust rust-lang profanity-detection profanity-filter rust-library crate profanity-check

rustrict's Introduction

rustrict

Documentation crates.io Build Test Page

rustrict is a profanity filter for Rust.

Disclaimer: Multiple source files (.txt, .csv, .rs test cases) contain profanity. Viewer discretion is advised.

Features

  • Multiple types (profane, offensive, sexual, mean, spam)
  • Multiple levels (mild, moderate, severe)
  • Resistant to evasion
    • Alternative spellings (like "fck")
    • Repeated characters (like "craaaap")
    • Confusable characters (like 'ᑭ', '𝕡', and '🅿')
    • Spacing (like "c r_a-p")
    • Accents (like "pÓöp")
    • Bidirectional Unicode (related reading)
    • Self-censoring (like "f*ck")
    • Safe phrase list for known bad actors]
    • Censors invalid Unicode characters
    • Battle-tested in Mk48.io
  • Resistant to false positives
    • One word (like "assassin")
    • Two words (like "push it")
  • Flexible
    • Censor and/or analyze
    • Input &str or Iterator<Item = char>
    • Can track per-user state with context feature
    • Can add words with the customize feature
    • Accurately reports the width of Unicode via the width feature
    • Plenty of options
  • Performant
    • O(n) analysis and censoring
    • No regex (uses custom trie)
    • 3 MB/s in release mode
    • 100 KB/s in debug mode

Limitations

  • Mostly English/emoji
  • Censoring removes most diacritics (accents)
  • Does not detect right-to-left profanity while analyzing, so...
  • Censoring forces Unicode to be left-to-right
  • Doesn't understand context
  • Not resistant to false positives affecting profanities added at runtime

Usage

Strings (&str)

use rustrict::CensorStr;

let censored: String = "hello crap".censor();
let inappropriate: bool = "f u c k".is_inappropriate();

assert_eq!(censored, "hello c***");
assert!(inappropriate);

Iterators (Iterator<Type = char>)

use rustrict::CensorIter;

let censored: String = "hello crap".chars().censor().collect();

assert_eq!(censored, "hello c***");

Advanced

By constructing a Censor, one can avoid scanning text multiple times to get a censored String and/or answer multiple is queries. This also opens up more customization options (defaults are below).

use rustrict::{Censor, Type};

let (censored, analysis) = Censor::from_str("123 Crap")
    .with_censor_threshold(Type::INAPPROPRIATE)
    .with_censor_first_character_threshold(Type::OFFENSIVE & Type::SEVERE)
    .with_ignore_false_positives(false)
    .with_ignore_self_censoring(false)
    .with_censor_replacement('*')
    .censor_and_analyze();

assert_eq!(censored, "123 C***");
assert!(analysis.is(Type::INAPPROPRIATE));
assert!(analysis.isnt(Type::PROFANE & Type::SEVERE | Type::SEXUAL));

If you cannot afford to let anything slip though, or have reason to believe a particular user is trying to evade the filter, you can check if their input matches a short list of safe strings:

use rustrict::{CensorStr, Type};

// Figure out if a user is trying to evade the filter.
assert!("pron".is(Type::EVASIVE));
assert!("porn".isnt(Type::EVASIVE));

// Only let safe messages through.
assert!("Hello there!".is(Type::SAFE));
assert!("nice work.".is(Type::SAFE));
assert!("yes".is(Type::SAFE));
assert!("NVM".is(Type::SAFE));
assert!("gtg".is(Type::SAFE));
assert!("not a common phrase".isnt(Type::SAFE));

If you want to add custom profanities or safe words, enable the customize feature.

#[cfg(feature = "customize")]
{
    use rustrict::{add_word, CensorStr, Type};

    // You must take care not to call these when the crate is being
    // used in any other way (to avoid concurrent mutation).
    unsafe {
        add_word("reallyreallybadword", (Type::PROFANE & Type::SEVERE) | Type::MEAN);
        add_word("mybrandname", Type::SAFE);
    }
    
    assert!("Reallllllyreallllllybaaaadword".is(Type::PROFANE));
    assert!("MyBrandName".is(Type::SAFE));
}

But wait, there's more! If your use-case is chat moderation, and you can store data on a per-user basis, you might benefit from the context feature.

#[cfg(feature = "context")]
{
    use rustrict::{BlockReason, Context};
    use std::time::Duration;
    
    pub struct User {
        context: Context,
    }
    
    let mut bob = User {
        context: Context::default()
    };
    
    // Ok messages go right through.
    assert_eq!(bob.context.process(String::from("hello")), Ok(String::from("hello")));
    
    // Bad words are censored.
    assert_eq!(bob.context.process(String::from("crap")), Ok(String::from("c***")));

    // Can take user reports (After many reports or inappropriate messages,
    // will only let known safe messages through.)
    for _ in 0..5 {
        bob.context.report();
    }
   
    // If many bad words are used or reports are made, the first letter of
    // future bad words starts getting censored too.
    assert_eq!(bob.context.process(String::from("crap")), Ok(String::from("****")));
    
    // Can manually mute.
    bob.context.mute_for(Duration::from_secs(2));
    assert!(matches!(bob.context.process(String::from("anything")), Err(BlockReason::Muted(_))));
}

Comparison

To compare filters, the first 100,000 items of this list is used as a dataset. Positive accuracy is the percentage of profanity detected as profanity. Negative accuracy is the percentage of clean text detected as clean.

Crate Accuracy Positive Accuracy Negative Accuracy Time
rustrict 79.74% 94.00% 76.18% 9s
censor 76.16% 72.76% 77.01% 23s

Development

Build

If you make an adjustment that would affect false positives, such as adding profanity, you will need to run false_positive_finder:

  1. Run make downloads to download the required word lists and dictionaries
  2. Run make false_positives to automatically find false positives

If you modify replacements_extra.csv, run make replacements to rebuild replacements.csv.

Finally, run make test for a full test or make test_debug for a fast test.

License

Licensed under either of

at your option.

Contribution

Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.

rustrict's People

Contributors

finnbear avatar sese008 avatar zsthai avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar

rustrict's Issues

Example: How to use trie

Motivation

add_word is marked deprecated and i have no how to add custom word with Trie, or Cencor::with_trie like it suggests. Small simple example would be great.

Filtering error (false positive and/or false negative)

False Positives

The following shouldn't have been detected, but was:

Bridge: Caleb Shomo

Context

I am using rustrict version latest

    let (censored, typ) = rustrict::Censor::from_str("Bridge: Caleb Shomo")
        .with_censor_first_character_threshold(Type::ANY)
        .with_censor_threshold(Type::ANY)
        .censor_and_analyze();

    dbg!(censored);
    println!("{:#b}", typ);

Output:

[src/main.rs:35] censored = "Bridge: Caleb S****"
0b1010000

UTF-8 added words not being detected

The following code does not work

use rustrict::{CensorStr, Type};
use rustrict::add_word;

fn main() {
    #[cfg(feature = "customize")]
    {
        unsafe {
            add_word("плохоеслово", Type::PROFANE & Type::SEVERE);
        }
    }

    let inappropriate = "hello плохоеслово".is_inappropriate();
    println!("{}", inappropriate); // false
}

Same with English chars does work

use rustrict::{CensorStr, Type};
use rustrict::add_word;

fn main() {
    #[cfg(feature = "customize")]
    {
        unsafe {
            add_word("badword", Type::PROFANE & Type::SEVERE);
        }
    }

    let inappropriate = "hello badword".is_inappropriate();
    println!("{}", inappropriate); // true
}

Also, is there a way to massively add new words?
Or maybe somehow extend the default one.

Context

I am using latest rustrict version (0.5.10).

Documentation: Extending the word list

It would be great if this project had documentation for extending the word lists. Especially if non-developers can contribute here, this library can evolve into a complete solution for profanity filtering.

If you can add the documentation, I can contribute to it for the Turkish language and hopefully get my company to contribute for Russian, Ukranian, and Indonesian, too!

Filtering error (false positive and/or false negative)

False Positives

The following shouldn't have been detected, but was:

False Negatives

The following should have been detected, but wasn't:

beanerino
beanermode
bootplug
booty2rooty
bootycall
bootyzz
cameljockey
cheekybollocs
cuckboi
cucklorde
cucklorde333
cucklorde666
cumgoblin
deez_noots
dickerlicker
dingleberry
fuker123
hellokinky666
ibekinky
jackoffcurly
jamesy_bondage
k1k3
kikekiller99
kikkee
kinky
kkklansman
lilcuck
lordqueef
nipple
penetratorhator
sexbot05
sexc dinos
sexmaster
sexmaster69
sextweek
stankbooty
thepenetrator
treefuker
ufuk

Context

We are using rustrict version 0.3.11 (via .is_inappropriate()) as part of our profanity filtering in picoCTF.

Thank you for creating this library! Even with the false negatives listed here, we still achieved about 90% effectiveness out of the box for our sample dataset.

In practice, we use the customization feature to work around most of these false negatives, but I thought that some may be of interest upstream.

Don't Mark Accents as Censored

Motivation

It is causing valid names to be marked as censored

Summary

While investigating why words with accented characters were marked as censored, I noticed this comment on the censor function

    /// # Unfortunate Side Effects
    ///
    /// All diacritical marks (accents) are removed by the current implementation. This is subject
    /// to change, as a better implementation would make this optional.

i made a test case to see the difference in text once run through censor and what it was marked as

    #[test]
    fn test() {
        let filter = RustrictFilter {
            ignore_false_positives: false,
            ignore_self_censoring: false,
            temp_censor_replacement: '\u{00a0}',
            regex: Regex::new(format!("{}+", '\u{00a0}').as_str()).unwrap(),
            censor_replacement: "🤫".to_string(),
        };

        let valid_name = "Ernésto Jose Durán Lar";
        println!("{}", valid_name);
        let result = filter.filter(valid_name, Severity::ModerateOrHigher);
        println!("{}", result);
        assert!(!filter.is_censored(valid_name, Severity::ModerateOrHigher));
    }

This outputs a failed test on the assert that the name is not censored:

Ernésto Jose Durán Lar
Ernesto Jose Duran Lar
thread 'filter::test::test' panicked at 'assertion failed: !filter.is_censored(valid_name, Severity::ModerateOrHigher)', libraries/s6-validations/src/filter.rs:218:9
stack backtrace:
   0: rust_begin_unwind
             at /rustc/8ede3aae28fe6e4d52b38157d7bfe0d3bceef225/library/std/src/panicking.rs:593:5
   1: core::panicking::panic_fmt
             at /rustc/8ede3aae28fe6e4d52b38157d7bfe0d3bceef225/library/core/src/panicking.rs:67:14
   2: core::panicking::panic
             at /rustc/8ede3aae28fe6e4d52b38157d7bfe0d3bceef225/library/core/src/panicking.rs:117:5
   3: s6_validations::filter::test::test
             at ./src/filter.rs:218:9
   4: s6_validations::filter::test::test::{{closure}}
             at ./src/filter.rs:205:15
   5: core::ops::function::FnOnce::call_once
             at /rustc/8ede3aae28fe6e4d52b38157d7bfe0d3bceef225/library/core/src/ops/function.rs:250:5
   6: core::ops::function::FnOnce::call_once
             at /rustc/8ede3aae28fe6e4d52b38157d7bfe0d3bceef225/library/core/src/ops/function.rs:250:5
note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace.
test filter::test::test ... FAILED

Could a change be made so that at minimum, this is not considered censored text?

Alternatives

Accented characters are not removed at all and they are not marked as censored

Context

I am using rustrict version 0.4.0

Filter falsely detects characters at the end of swears as part of the swear

(This was taken from this bartender issue.)

If we try to run censor on the text "fuck", it returns "f***" as expected. The issue arises, however, when we attempt to implement a markdown parser like Earmark. This runs us into a predicament, because if we run censor before we run as_html, it would parse the censor as markdown. If we run censor after we run as_html, it would parse self censoring as markdown in some cases, sabotaging the filter. Running it after also makes "<p>fuck</p>" return "<p>f****/p>". We could try to make it run an iterator over some html to censor it, but then we would run into the same issue with self censoring.

I will probably just remove these tags that Earmark/Pulldown adds. I am just posting this issue so that it's known.

EDIT: Version is 0.7.24

Filtering error (false positive)

False Positives

The following shouldn't have been detected, but was:

ain't it
really didn't (It turns out)
honkey-tonk // I suppose honkey-tonk is not a bad word, but I don't really know
I get lost

Context

I am using rustrict version 0.3.14

Code sample:

    let cases = [
        "ain't it",
        "really didn't (It turns out)",
        "honkey-tonk",
        "I get lost",
    ];

    cases.map(|case| {
        let (censored, typ) = rustrict::Censor::from_str(case)
            .with_censor_first_character_threshold(Type::ANY)
            .with_censor_threshold(Type::ANY)
            .censor_and_analyze();

        println!(
            "{:?} {:?} {:#b} ({})",
            case,
            censored,
            typ,
            TypeWrapper(typ)
        );
    });

Outputs

"ain't it" "ain'****" 0b10000000 (moderate sexual 🟠)
"really didn't (It turns out)" "really didn'***** turns out)" 0b10000000 (moderate sexual 🟠)
"honkey-tonk" "******-tonk" 0b10000 (moderate offensive 🟠)
"I get lost" "I ********" 0b10000000000 (moderate mean 🟠)

False negatives

False Negatives

The following should have been detected, but wasn't:

fuq
What the fuq

Context

I am using rustrict version 0.4.0, which is currently the latest version.

I discovered the false negative while changing the suffix of the word fuck.

Self-censoring & accents does not work with custom non English words

When adding a custom non English word, everything works fine except self-censoring and accents

unsafe {
    add_word("плохоеслово", Type::PROFANE & Type::SEVERE);
    add_word("badword", Type::PROFANE & Type::SEVERE);
}

assert!("b*d w***r-d тест".is(Type::INAPPROPRIATE)); // true
assert!("badwörd тест".is(Type::INAPPROPRIATE)); // true

assert!("плохоеслово тест".is(Type::INAPPROPRIATE)); // true
assert!("п л о х о е с   л о  в о тест".is(Type::INAPPROPRIATE)); // true
assert!("плоооохоооое слово тест".is(Type::INAPPROPRIATE)); // true
assert!("п__л--о о о о х_о_о_о_о-е слово тест".is(Type::INAPPROPRIATE)); // true

assert!("пл*х*есл*во тест".is(Type::INAPPROPRIATE)); // false
assert!("плöхöеслöвö тест".is(Type::INAPPROPRIATE)); // false

Also, is there a way to add custom confusable characters?
Or we should generate custom variants for each added word.

Context

I am using rustrict version 0.5.11 (latest version)

Feature request

people in my chat app have been using the character '∁' to get around this beautiful filter
I will also be submitting issues or PRs for anything that evades your filter.
In rustrict we trust.

(the version I'm using is whatever one was in the bindings I made for Elixir, called Bartender.

Filtering error (false positive and/or false negative)

False Positives

The following shouldn't have been detected, but was:

gay

False Negatives

The following should have been detected, but wasn't:


Context

I am using rustrict version v0.7.6

I am using .with_censor_threshold(Type::MODERATE_OR_HIGHER)

This assertion comes back true:
let analysis = Censor::from_str("gay").analyze();
assert!(analysis.is(Type::MILD & Type::OFFENSIVE));

println!("gay -> {}", "gay".censor());
gay -> g**

False positive cases

I use this lib to check profanity in songs' lyrics. I really thankful about "glass" doesn't have "ass" in :) But I have some false positive cases I want to share:

  • I could say I miss you but it’s not the truth (severe sexual)
  • But this time only the hurt inside is what is real (mild profane)
  • You said your mother only smiled on her TV show (mild sexual mean)
  • I'm holding on to your love but they won't stop 'til it's dead, (severe sexual)
  • I know you hate yourself but that doesn't change, (moderate profane mean)

I highlighted detected words

CGO Wrapper

Motivation

Given the Go-based version, is not longer actively maintained, it would be nice to still be able to call this from Go. The request is for the creation of a CGO wrapper maintained in this repository to ensure API changes are part of the branching strategy of this repository.

Summary

Alternatives

I'd prefer not to fork moderation and maintain it or find an Go-based alternative.

Context

I am not using rustrict but am using moderation.

Reading multiple files

I have to say it wasn't hard at all to get, roughly, what the code was doing, and I appreciate it very much.
I have a suggestion, I find it interesting to have a function which reads a file and it assigns all the words in itself to a specific Type. It actually is what you're doing. My suggestion is to implement a specific funcition which does it, so you can easly add many new words. I am devoloping an app which will come out in Italy, that is the reson why I needed to add new words ;)

Filtering error (false positive)

Hello there. It's me again with my profanity detector for Spotify songs. After upgrade I've got some interesting cases:

Detected parts framed with brackets

False Positives

P(re-C)hours (lots of them, I don't even know which wort is it)
(Nig)thmares (there's a typo in word but still funny case)
Quo (fug)iam ab eorum spiritibus (I guess it's Latin language. I feel, You can ignore this edge case)

Context

I am using rustrict version rustrict = { version = "0.7.6", features = ["customize"] }

Question aside this issue: can I somehow get list of all detected words with its plain form? For example, rustrict redacted s-h-i-t and I get vector with one word vec!["shit"] back

PS: Thanks for the wonderful library. Good part of my project based on it ❤️

Add tags to every published version

I love exploring source code of my favourite libs. In this case, I especially need it to compare changes in dictionaries. It's not very convenient to search commit history

Typing in Context?

Hi, I was reading over the docs, and I couldn't find a way to censor Type::SEVERE | Type::OFFENSIVE only. Is there a way to do this already? If not, could this be implemented? Thanks!

EDIT: Forgot to mention this is to do with the context feature 😅

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.