rustrict/
context.rs

1use crate::{trim_whitespace, Censor, Type};
2
3use crate::censor::should_skip_censor;
4use std::collections::VecDeque;
5use std::fmt::{self, Debug, Display, Formatter};
6use std::num::{NonZeroU16, NonZeroUsize};
7use std::time::{Duration, Instant};
8
9/// Context is useful for taking moderation actions on a per-user basis i.e. each user would get
10/// their own Context.
11///
12/// # Recommendation
13///
14/// Use this as a reference implementation e.g. by copying and adapting it.
15#[derive(Clone)]
16#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
17#[cfg_attr(doc, doc(cfg(feature = "context")))]
18pub struct Context {
19    history: VecDeque<(String, Time)>,
20    burst_used: u8,
21    suspicion: u8,
22    reports: u8,
23    total: u16,
24    total_inappropriate: u16,
25    muted_until: Option<Time>,
26    only_safe_until: Option<Time>,
27    rate_limited_until: Option<Time>,
28    last_message: Option<Time>,
29}
30
31impl Debug for Context {
32    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
33        // Don't debug history field.
34        f.debug_struct("Context")
35            .field("burst_used", &self.burst_used)
36            .field("suspicion", &self.suspicion)
37            .field("reports", &self.reports)
38            .field("total", &self.total)
39            .field("total_inappropriate", &self.total_inappropriate)
40            .field("muted_until", &self.muted_until)
41            .field("only_safe_until", &self.only_safe_until)
42            .field("rate_limited_until", &self.rate_limited_until)
43            .field("last_message", &self.last_message)
44            .finish_non_exhaustive()
45    }
46}
47
48/// Options for customizing `Context::process_with_options`. Always initialize with ..Default::default(),
49/// as new fields may be added in the future.
50#[derive(Clone, Debug)]
51#[cfg_attr(doc, doc(cfg(feature = "context")))]
52pub struct ContextProcessingOptions {
53    /// Block messages if the user has been manually muted.
54    pub block_if_muted: bool,
55    /// Block messages if they are empty (after whitespace is trimmed, if applicable).
56    pub block_if_empty: bool,
57    /// Block messages, as opposed to censoring, if severe inappropriateness is detected.
58    pub block_if_severely_inappropriate: bool,
59    /// Block all messages if they are unsafe (useful for implementing moderator-activated "safe mode").
60    /// Note that unsafe messages from certain users may also be blocked automatically.
61    pub safe_mode_until: Option<Instant>,
62    /// Character count (or, with the `width` feature, number of `m`-equivalent widths).
63    ///
64    /// Messages will be trimmed to fit.
65    pub character_limit: Option<NonZeroUsize>,
66    /// Ensure word-break will work on the message.
67    #[cfg(feature = "width")]
68    pub word_break: Option<ContextWordBreakOptions>,
69    /// Rate-limiting options.
70    pub rate_limit: Option<ContextRateLimitOptions>,
71    /// Block messages if they are very similar to this many previous message.
72    pub repetition_limit: Option<ContextRepetitionLimitOptions>,
73    /// Maximum automatic "safe" timeouts can last. If set too high, users have more time/incentive to
74    /// try and find ways around the system. If zero, "safe" timeouts won't be used.
75    pub max_safe_timeout: Duration,
76    /// Trim whitespace from beginning and end before returning censored output.
77    pub trim_whitespace: bool,
78}
79
80impl Default for ContextProcessingOptions {
81    fn default() -> Self {
82        Self {
83            block_if_muted: true,
84            block_if_empty: true,
85            block_if_severely_inappropriate: true,
86            safe_mode_until: None,
87            character_limit: Some(NonZeroUsize::new(2048).unwrap()),
88            rate_limit: Some(ContextRateLimitOptions::default()),
89            #[cfg(feature = "width")]
90            word_break: Some(ContextWordBreakOptions::default()),
91            repetition_limit: Some(ContextRepetitionLimitOptions::default()),
92            max_safe_timeout: Duration::from_secs(30 * 60),
93            trim_whitespace: true,
94        }
95    }
96}
97
98/// Options that control rate-limiting.
99#[derive(Clone, Debug)]
100#[cfg_attr(doc, doc(cfg(feature = "context")))]
101pub struct ContextRateLimitOptions {
102    /// Minimum time between messages (zero means infinite rate, 2s means 0.5 messages per second).
103    pub limit: Duration,
104    /// Allows a certain amount of messages beyond the rate limit.
105    pub burst: u8,
106    /// Count a message against the rate limit up to 3 times, once for each unit of this many characters.
107    ///
108    /// If the `width` feature is enabled, the length of the text is interpreted as the number
109    /// of `m`'s it would take to reach the same length, or the number of characters, whichever
110    /// is higher.
111    pub character_limit: Option<NonZeroU16>,
112}
113
114impl Default for ContextRateLimitOptions {
115    fn default() -> Self {
116        Self {
117            limit: Duration::from_secs(5),
118            burst: 3,
119            character_limit: Some(NonZeroU16::new(16).unwrap()),
120        }
121    }
122}
123
124impl ContextRateLimitOptions {
125    /// Alternate defaults for slow mode.
126    pub fn slow_mode() -> Self {
127        Self {
128            limit: Duration::from_secs(10),
129            burst: 2,
130            character_limit: Some(NonZeroU16::new(10).unwrap()),
131        }
132    }
133}
134
135/// Options that ensure word break will be possible.
136#[derive(Clone, Debug)]
137#[cfg(feature = "width")]
138#[cfg_attr(doc, doc(cfg(all(feature = "context", feature = "width"))))]
139pub struct ContextWordBreakOptions {
140    /// The type of word-breaking used to display the text.
141    pub word_break: crate::width::WordBreak,
142    /// The maximum length of an unbreakable part (before the entire message is blocked).
143    pub limit: NonZeroUsize,
144}
145
146#[cfg(feature = "width")]
147impl Default for ContextWordBreakOptions {
148    fn default() -> Self {
149        Self {
150            word_break: crate::width::WordBreak::BreakAll,
151            limit: NonZeroUsize::new(16).unwrap(),
152        }
153    }
154}
155
156/// Options that control repetition-limiting.
157#[derive(Clone, Debug)]
158#[cfg_attr(doc, doc(cfg(feature = "context")))]
159pub struct ContextRepetitionLimitOptions {
160    /// How many recent strings can be similar before blocking ensues.
161    pub limit: u8,
162    /// How long recent input is remembered for.
163    pub memory: Duration,
164    /// Normalized levenshtein threshold to consider "too similar."
165    pub similarity_threshold: f32,
166}
167
168impl Default for ContextRepetitionLimitOptions {
169    fn default() -> Self {
170        Self {
171            limit: 3,
172            memory: Duration::from_secs(60),
173            similarity_threshold: 2.0 / 3.0,
174        }
175    }
176}
177
178impl Context {
179    pub fn new() -> Self {
180        Self {
181            history: VecDeque::new(),
182            burst_used: 0,
183            suspicion: 0,
184            reports: 0,
185            total: 0,
186            total_inappropriate: 0,
187            only_safe_until: None,
188            rate_limited_until: None,
189            muted_until: None,
190            last_message: None,
191        }
192    }
193
194    /// Returns None if expired is None or has been reached, resulting in expiry being set to None.
195    /// Otherwise, returns duration before expiry.
196    fn remaining_duration(expiry: &mut Option<Time>, now: Instant) -> Option<Duration> {
197        if let Some(time) = *expiry {
198            if now >= time.0 {
199                *expiry = None;
200                None
201            } else {
202                Some(time.0 - now)
203            }
204        } else {
205            None
206        }
207    }
208
209    /// Takes user message, returns censored message trimmed of whitespace (if it should be sent)
210    /// or `BlockReason` (explaining why it should be blocked entirely).
211    pub fn process(&mut self, message: String) -> Result<String, BlockReason> {
212        self.process_with_options(message, &ContextProcessingOptions::default())
213    }
214
215    /// Takes user message, returns censored message trimmed of whitespace (if it should be sent)
216    /// or `BlockReason` (explaining why it should be blocked entirely).
217    ///
218    /// Takes a set of options for fine-tuning the processing.
219    pub fn process_with_options(
220        &mut self,
221        message: String,
222        options: &ContextProcessingOptions,
223    ) -> Result<String, BlockReason> {
224        let now = Instant::now();
225        let elapsed = self
226            .last_message
227            .map(|l| now.saturating_duration_since(l.0))
228            .unwrap_or(Duration::ZERO);
229
230        let suspicion = self.suspicion.max(1).saturating_mul(self.reports.max(1));
231
232        // How convinced are we that the user is a bad actor.
233        let is_kinda_sus = suspicion >= 2;
234        let is_impostor = suspicion >= 15;
235
236        // Don't give bad actors the benefit of the doubt when it comes to meanness.
237        let meanness_threshold = if is_impostor {
238            Type::MILD_OR_HIGHER
239        } else if is_kinda_sus {
240            Type::MODERATE_OR_HIGHER
241        } else {
242            Type::SEVERE
243        };
244
245        let censor_threshold =
246            Type::PROFANE | Type::OFFENSIVE | Type::SEXUAL | (Type::MEAN & meanness_threshold);
247
248        // Don't give bad actors the benefit of letting their first character through.
249        let censor_first_character_threshold = if is_kinda_sus {
250            censor_threshold
251        } else {
252            // Mainly for protection against the n-word being discernible.
253            Type::OFFENSIVE & Type::SEVERE
254        };
255
256        let (mut censored, analysis) = Censor::from_str(&message)
257            .with_censor_threshold(censor_threshold)
258            .with_censor_first_character_threshold(censor_first_character_threshold)
259            .censor_and_analyze();
260
261        let mut censored_str = if should_skip_censor(&message) {
262            message.as_str()
263        } else {
264            censored.as_str()
265        };
266
267        if let Some(character_limit) = options.character_limit {
268            #[cfg(feature = "width")]
269            {
270                censored_str = crate::trim_to_width(censored_str, character_limit.get());
271            }
272            if let Some((limit, _)) = censored_str.char_indices().nth(character_limit.get()) {
273                censored_str = &censored_str[..limit];
274            }
275        }
276
277        if options.trim_whitespace {
278            censored_str = trim_whitespace(censored_str);
279        }
280
281        #[cfg(feature = "width")]
282        {
283            if let Some(word_break) = &options.word_break {
284                let max = crate::width::width_str_max_unbroken(censored_str, word_break.word_break);
285                if max > word_break.limit.get() {
286                    return Err(BlockReason::Unbroken(max));
287                }
288            }
289        }
290
291        if censored_str.len() < censored.len() {
292            // Something was trimmed, must must re-allocate.
293            censored = String::from(censored_str);
294        }
295
296        self.total = self.total.saturating_add(1);
297        if analysis.is(Type::INAPPROPRIATE) {
298            self.total_inappropriate = self.total_inappropriate.saturating_add(1);
299        }
300
301        // Collecting suspicion.
302        let type_to_sus = |typ: Type| -> u8 {
303            let combined = analysis & typ;
304            if combined.is(Type::SEVERE) {
305                3
306            } else if combined.is(Type::MODERATE) {
307                2
308            } else if combined.is(Type::MILD) {
309                1
310            } else {
311                0
312            }
313        };
314
315        // Repetition detection.
316        let mut recent_similar = 0;
317
318        if let Some(opts) = options.repetition_limit.as_ref() {
319            self.history.retain(|&(_, t)| now - t.0 < opts.memory);
320
321            for (recent_message, _) in &self.history {
322                if strsim::normalized_levenshtein(recent_message, &message)
323                    >= opts.similarity_threshold as f64
324                {
325                    recent_similar += 1;
326                }
327            }
328        }
329
330        let mut new_suspicion = type_to_sus(Type::PROFANE | Type::OFFENSIVE | Type::SEXUAL)
331            + type_to_sus(Type::EVASIVE)
332            + type_to_sus(Type::SPAM);
333
334        if recent_similar >= 2 {
335            // Don't penalize as much for repeated messages, since an innocent user may repeat their
336            // message multiple times if it was erroneously detected.
337            new_suspicion /= 2;
338        }
339
340        if ((is_kinda_sus && new_suspicion >= 4) || (is_impostor && new_suspicion >= 2))
341            && !options.max_safe_timeout.is_zero()
342        {
343            if let Some(only_safe_until) = self
344                .only_safe_until
345                .map(|t| t.0)
346                .unwrap_or(now)
347                .checked_add(if self.reports > 0 {
348                    Duration::from_secs(10 * 60)
349                } else {
350                    Duration::from_secs(5 * 60)
351                })
352            {
353                self.only_safe_until =
354                    Some(Time(only_safe_until.min(now + options.max_safe_timeout)));
355            }
356        }
357
358        self.suspicion = self.suspicion.saturating_add(new_suspicion);
359
360        let remaining_rate_limit = Self::remaining_duration(&mut self.rate_limited_until, now);
361
362        if let Some(remaining) = options
363            .safe_mode_until
364            .filter(|_| analysis.isnt(Type::SAFE))
365            .and_then(|until| until.checked_duration_since(now))
366        {
367            Err(BlockReason::Unsafe {
368                remaining,
369                targeted: false,
370            })
371        } else if let Some(dur) =
372            Self::remaining_duration(&mut self.muted_until, now).filter(|_| options.block_if_muted)
373        {
374            Err(BlockReason::Muted(dur))
375        } else if options.block_if_empty && censored.is_empty() {
376            Err(BlockReason::Empty)
377        } else if let Some(dur) = options
378            .rate_limit
379            .as_ref()
380            .and_then(|opt| remaining_rate_limit.filter(|_| self.burst_used >= opt.burst))
381        {
382            Err(BlockReason::Spam(dur))
383        } else if options
384            .repetition_limit
385            .as_ref()
386            .map(|opts| recent_similar >= opts.limit)
387            .unwrap_or(false)
388        {
389            Err(BlockReason::Repetitious(recent_similar as usize))
390        } else if options.block_if_severely_inappropriate
391            && analysis.is(Type::INAPPROPRIATE & Type::SEVERE)
392        {
393            Err(BlockReason::Inappropriate(analysis))
394        } else if let Some(remaining) = Self::remaining_duration(&mut self.only_safe_until, now)
395            .filter(|_| !(analysis.is(Type::SAFE) || options.max_safe_timeout.is_zero()))
396        {
397            Err(BlockReason::Unsafe {
398                remaining,
399                targeted: true,
400            })
401        } else {
402            self.last_message = Some(Time(now));
403            if let Some(rate_limit_options) = options.rate_limit.as_ref() {
404                // How many messages does this count for against the rate limit.
405                let rate_limit_messages =
406                    if let Some(char_limit) = rate_limit_options.character_limit {
407                        let char_count = message.chars().count();
408
409                        #[cfg(feature = "width")]
410                        let char_count = char_count.max(crate::width_str(&message));
411
412                        (char_count / char_limit.get() as usize).clamp(1, 3) as u8
413                    } else {
414                        1
415                    };
416
417                self.burst_used = if remaining_rate_limit.is_some() {
418                    self.burst_used.saturating_add(rate_limit_messages)
419                } else {
420                    self.burst_used.saturating_sub(
421                        (elapsed.as_nanos() / rate_limit_options.limit.as_nanos())
422                            .min(u8::MAX as u128) as u8,
423                    )
424                };
425                if let Some(rate_limited_until) = self
426                    .rate_limited_until
427                    .map(|t| t.0)
428                    .unwrap_or(now)
429                    .checked_add(
430                        rate_limit_options.limit * (rate_limit_messages + new_suspicion) as u32,
431                    )
432                {
433                    self.rate_limited_until = Some(Time(rate_limited_until));
434                }
435            }
436            // Forgiveness (minus one suspicion per safe message, and also per minute between messages).
437            self.suspicion = self.suspicion.saturating_sub(
438                (elapsed.as_secs() / 60).clamp(analysis.is(Type::SAFE) as u64, u8::MAX as u64)
439                    as u8,
440            );
441
442            if let Some(repetition_blocking_options) = options.repetition_limit.as_ref() {
443                if self.history.len() >= repetition_blocking_options.limit as usize * 2 {
444                    self.history.pop_front();
445                }
446
447                self.history.push_back((message, Time(now)));
448            }
449
450            Ok(censored)
451        }
452    }
453
454    /// Returns how long the user is muted for (possibly [`Duration::ZERO`]).
455    pub fn muted_for(&self) -> Duration {
456        self.muted_until
457            .map(|muted_until| muted_until.0.saturating_duration_since(Instant::now()))
458            .unwrap_or(Duration::ZERO)
459    }
460
461    /// Returns the instant of the last processed message.
462    pub fn last_message(&self) -> Option<Instant> {
463        self.last_message.map(|t| t.0)
464    }
465
466    /// Returns the latest instant the user is muted (possibly in the past).
467    pub fn muted_until(&self) -> Option<Instant> {
468        self.muted_until.map(|t| t.0)
469    }
470
471    /// Returns how long the user is restricted to [`Type::SAFE`] for (possibly [`Duration::ZERO`]).
472    pub fn restricted_for(&self) -> Duration {
473        self.only_safe_until
474            .map(|restricted_until| restricted_until.0.saturating_duration_since(Instant::now()))
475            .unwrap_or(Duration::ZERO)
476    }
477
478    /// Returns the latest instant the user is restricted (possibly in the past).
479    pub fn restricted_until(&self) -> Option<Instant> {
480        self.only_safe_until.map(|t| t.0)
481    }
482
483    /// Manually mute this user's messages for a duration. Overwrites any previous manual mute.
484    /// Passing `Duration::ZERO` will therefore un-mute.
485    pub fn mute_for(&mut self, duration: Duration) {
486        self.mute_until(Instant::now() + duration);
487    }
488
489    /// Manually mute this user's messages until an instant. Overwrites any previous manual mute.
490    /// Passing an instant in the past will therefore un-mute.
491    pub fn mute_until(&mut self, instant: Instant) {
492        self.muted_until = Some(Time(instant));
493    }
494
495    /// Manually restrict this user's messages to known safe phrases for a duration. Overwrites any
496    /// previous manual restriction. Passing `Duration::ZERO` will therefore un-restrict.
497    pub fn restrict_for(&mut self, duration: Duration) {
498        self.restrict_until(Instant::now() + duration);
499    }
500
501    /// Manually restrict this user's messages to known safe phrases until an instant. Overwrites any
502    /// previous manual restriction. Passing an instant in the past will therefore un-restrict.
503    pub fn restrict_until(&mut self, instant: Instant) {
504        self.only_safe_until = Some(Time(instant));
505    }
506
507    /// Call if another user "reports" this user's message(s). The function of reports is for
508    /// suspicion of bad behavior to be confirmed faster.
509    pub fn report(&mut self) {
510        self.reports = self.reports.saturating_add(1);
511    }
512
513    /// Returns number of reports received via `Self::report()`. It is not guaranteed that the full
514    /// range of `usize` of reports will be counted (currently only `u8::MAX` are counted).
515    pub fn reports(&self) -> usize {
516        self.reports as usize
517    }
518
519    /// Clear suspicion, reports, inappropriate counter, and automatic mutes (not manual mute or rate limit).
520    pub fn exonerate(&mut self) {
521        self.total_inappropriate = 0;
522        self.suspicion = 0;
523        self.reports = 0;
524        self.only_safe_until = None;
525    }
526
527    /// Returns total number of messages processed. It is not guaranteed that the full
528    /// range of `usize` of messages will be counted (currently only `u16::MAX` are counted).
529    pub fn total(&self) -> usize {
530        self.total as usize
531    }
532
533    /// Returns total number of messages processed that were `Type::INAPPROPRIATE`. It is not
534    /// guaranteed that the full range of `usize` of messages will be counted (currently only
535    /// `u16::MAX` are counted).
536    pub fn total_inappropriate(&self) -> usize {
537        self.total_inappropriate as usize
538    }
539}
540
541impl Default for Context {
542    fn default() -> Self {
543        Self::new()
544    }
545}
546
547/// Communicates why a message was blocked as opposed to merely censored.
548#[derive(Copy, Clone, Debug, PartialEq)]
549#[non_exhaustive]
550#[cfg_attr(doc, doc(cfg(feature = "context")))]
551pub enum BlockReason {
552    /// The particular message was *severely* inappropriate, more specifically, `Type`.
553    Inappropriate(Type),
554    #[cfg(feature = "width")]
555    /// There was an unbroken part of the string of this length, exceeding the limit.
556    Unbroken(usize),
557    /// Recent messages were generally inappropriate, and this message isn't on the safe list.
558    /// Alternatively, if targeted is false, safe mode was configured globally.
559    /// Try again after `Duration`.
560    Unsafe {
561        remaining: Duration,
562        /// Whether unsafe mode was targeted at this user (as opposed to configured globally).
563        targeted: bool,
564    },
565    /// This message was too similar to `usize` recent messages.
566    Repetitious(usize),
567    /// Too many messages per unit time, try again after `Duration`.
568    Spam(Duration),
569    /// Manually muted for `Duration`.
570    Muted(Duration),
571    /// Message was, at least after censoring, completely empty.
572    Empty,
573}
574
575impl BlockReason {
576    /// You may display `BlockReason` in any manner you choose, but this will return a reasonable
577    /// default warning to send to the user.
578    pub fn generic_str(self) -> &'static str {
579        match self {
580            Self::Inappropriate(typ) => {
581                if typ.is(Type::OFFENSIVE) {
582                    "Your message was held for being highly offensive"
583                } else if typ.is(Type::SEXUAL) {
584                    "Your message was held for being overly sexual"
585                } else if typ.is(Type::MEAN) {
586                    "Your message was held for being overly mean"
587                } else {
588                    "Your message was held for severe profanity"
589                }
590            }
591            #[cfg(feature = "width")]
592            Self::Unbroken(_) => "Part of your message is too wide to display",
593            Self::Unsafe { .. } => "You have been temporarily restricted due to profanity/spam",
594            Self::Repetitious(_) => "Your message was too similar to recent messages",
595            Self::Spam(_) => "You have been temporarily muted due to excessive frequency",
596            Self::Muted(_) => "You have been temporarily muted",
597            Self::Empty => "Your message was empty",
598        }
599    }
600
601    #[deprecated = "use contextual_string"]
602    pub fn contextual_str(self) -> String {
603        self.contextual_string()
604    }
605
606    /// You may display `BlockReason` in any manner you choose, but this will return a reasonable
607    /// default warning to send to the user that includes some context (such as how long they are
608    /// muted for).
609    pub fn contextual_string(self) -> String {
610        match self {
611            Self::Unsafe {
612                remaining,
613                targeted: true,
614            } => format!(
615                "You have been restricted for {} due to profanity/spam",
616                FormattedDuration(remaining)
617            ),
618            Self::Unsafe {
619                remaining,
620                targeted: false,
621            } => format!("Safe mode is active for {}", FormattedDuration(remaining)),
622            Self::Repetitious(count) => {
623                format!("Your message was too similar to {} recent messages", count)
624            }
625            Self::Spam(dur) => format!(
626                "You have been muted for {} due to excessive frequency",
627                FormattedDuration(dur)
628            ),
629            Self::Muted(dur) => format!("You have been muted for {}", FormattedDuration(dur)),
630            _ => self.generic_str().to_owned(),
631        }
632    }
633}
634
635struct FormattedDuration(Duration);
636
637impl Display for FormattedDuration {
638    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
639        if self.0 >= Duration::from_secs(3600) {
640            write!(f, "{}h", self.0.as_secs() / 3600)
641        } else if self.0 >= Duration::from_secs(60) {
642            write!(f, "{}m", self.0.as_secs() / 60)
643        } else {
644            write!(f, "{}s", self.0.as_secs().max(1))
645        }
646    }
647}
648
649#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd)]
650#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
651struct Time(#[cfg_attr(feature = "serde", serde(with = "approx_instant"))] Instant);
652
653impl Debug for Time {
654    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
655        Debug::fmt(&self.0, f)
656    }
657}
658
659#[cfg(feature = "serde")]
660mod approx_instant {
661    use serde::{de::Error, Deserialize, Deserializer, Serialize, Serializer};
662    use std::time::{Duration, Instant, SystemTime};
663
664    pub fn serialize<S>(instant: &Instant, serializer: S) -> Result<S::Ok, S::Error>
665    where
666        S: Serializer,
667    {
668        let system_now = SystemTime::now();
669        let instant_now = Instant::now();
670        let approx = if instant_now > *instant {
671            system_now - (instant_now - *instant)
672        } else {
673            system_now + (*instant - instant_now)
674        };
675        let millis = approx
676            .duration_since(SystemTime::UNIX_EPOCH)
677            .unwrap_or_default()
678            .as_millis() as u64;
679        millis.serialize(serializer)
680    }
681
682    pub fn deserialize<'de, D>(deserializer: D) -> Result<Instant, D::Error>
683    where
684        D: Deserializer<'de>,
685    {
686        let millis = u64::deserialize(deserializer)?;
687        let system_now = SystemTime::now();
688        let de = SystemTime::UNIX_EPOCH
689            .checked_add(Duration::from_millis(millis))
690            .unwrap_or(system_now);
691        let instant_now = Instant::now();
692        let approx = if system_now > de {
693            let duration = system_now.duration_since(de).map_err(Error::custom)?;
694            instant_now - duration
695        } else {
696            let duration = de.duration_since(system_now).map_err(Error::custom)?;
697            instant_now + duration
698        };
699        Ok(approx)
700    }
701}
702
703#[cfg(test)]
704mod tests {
705    #![allow(unused_imports)]
706
707    extern crate test;
708    use crate::context::{
709        ContextProcessingOptions, ContextRateLimitOptions, ContextRepetitionLimitOptions,
710    };
711    use crate::{Censor, CensorIter, CensorStr, Type};
712    use serial_test::serial;
713    use std::fs::File;
714    use std::io::BufReader;
715    use std::num::NonZeroUsize;
716    use std::time::{Duration, Instant};
717    use test::Bencher;
718
719    #[test]
720    fn context_inappropriate() {
721        use crate::{BlockReason, Context};
722
723        let mut ctx = Context::new();
724
725        assert_eq!(ctx.process(String::from("one")), Ok(String::from("one")));
726        assert!(matches!(
727            ctx.process(String::from("nigga")),
728            Err(BlockReason::Inappropriate(_))
729        ));
730    }
731
732    #[test]
733    fn context_unsafe() {
734        use crate::{BlockReason, Context};
735
736        let mut ctx = Context::new();
737
738        for _ in 0..30 {
739            ctx.report();
740        }
741
742        let res = ctx.process(String::from("shit"));
743        assert!(
744            matches!(res, Err(BlockReason::Unsafe { targeted: true, .. })),
745            "1 {:?}",
746            res
747        );
748
749        let res = ctx.process(String::from("not common message"));
750        assert!(
751            matches!(res, Err(BlockReason::Unsafe { targeted: true, .. })),
752            "2 {:?}",
753            res
754        );
755    }
756
757    #[test]
758    fn context_repetitious() {
759        use crate::{BlockReason, Context};
760
761        let mut ctx = Context::new();
762
763        for _ in 0..ContextRepetitionLimitOptions::default().limit {
764            assert!(ctx.process(String::from("one")).is_ok());
765        }
766
767        let res = ctx.process(String::from("onne"));
768        assert!(matches!(res, Err(BlockReason::Repetitious(_))), "{:?}", res);
769    }
770
771    #[test]
772    #[serial]
773    fn context_spam() {
774        use crate::{BlockReason, Context};
775
776        let mut ctx = Context::new();
777        let opts = ContextProcessingOptions {
778            rate_limit: Some(ContextRateLimitOptions {
779                limit: Duration::from_millis(350),
780                burst: 2,
781                ..Default::default()
782            }),
783            ..Default::default()
784        };
785
786        assert_eq!(
787            ctx.process_with_options(String::from("one"), &opts),
788            Ok(String::from("one"))
789        );
790        assert_eq!(
791            ctx.process_with_options(String::from("two"), &opts),
792            Ok(String::from("two"))
793        );
794        assert_eq!(
795            ctx.process_with_options(String::from("three"), &opts),
796            Ok(String::from("three"))
797        );
798        let res = ctx.process_with_options(String::from("four"), &opts);
799        assert!(matches!(res, Err(BlockReason::Spam(_))), "{:?}", res);
800
801        std::thread::sleep(Duration::from_secs(2));
802
803        assert_eq!(
804            ctx.process_with_options(String::from("one"), &opts),
805            Ok(String::from("one"))
806        );
807    }
808
809    #[test]
810    #[serial]
811    fn context_spam_long_message() {
812        use crate::{BlockReason, Context};
813
814        let mut ctx = Context::new();
815        let opts = ContextProcessingOptions {
816            rate_limit: Some(ContextRateLimitOptions {
817                limit: Duration::from_millis(350),
818                burst: 2,
819                ..Default::default()
820            }),
821            ..Default::default()
822        };
823
824        assert_eq!(
825            ctx.process_with_options(String::from("three"), &opts),
826            Ok(String::from("three"))
827        );
828        assert!(ctx.process_with_options(String::from("one two three one two three one two three one two three one two three one two three one two three one two three one two three"), &opts).is_ok());
829        let result = ctx.process_with_options(String::from("four"), &opts);
830        assert!(matches!(result, Err(BlockReason::Spam(_))), "{:?}", result);
831    }
832
833    #[test]
834    fn context_muted() {
835        use crate::{BlockReason, Context};
836
837        let mut ctx = Context::new();
838
839        ctx.mute_for(Duration::from_secs(5));
840
841        let res = ctx.process(String::from("hello"));
842        assert!(matches!(res, Err(BlockReason::Muted(_))), "{:?}", res);
843    }
844
845    #[test]
846    fn context_safe_mode() {
847        use crate::{BlockReason, Context};
848
849        let mut ctx = Context::new();
850
851        let res = ctx.process_with_options(
852            String::from("not on the safe list"),
853            &ContextProcessingOptions {
854                safe_mode_until: Some(Instant::now() + Duration::from_secs(100)),
855                ..Default::default()
856            },
857        );
858        assert!(
859            matches!(
860                res,
861                Err(BlockReason::Unsafe {
862                    targeted: false,
863                    ..
864                })
865            ),
866            "{:?}",
867            res
868        );
869    }
870
871    #[test]
872    fn context_empty() {
873        use crate::{BlockReason, Context};
874
875        let mut ctx = Context::new();
876        assert_eq!(ctx.process(String::from("   ")), Err(BlockReason::Empty));
877    }
878
879    #[test]
880    #[cfg(feature = "width")]
881    fn character_limit() {
882        use crate::{
883            context::ContextWordBreakOptions, BlockReason, Context, ContextProcessingOptions,
884        };
885        let mut ctx = Context::new();
886
887        let opts = ContextProcessingOptions {
888            character_limit: Some(NonZeroUsize::new(5).unwrap()),
889            word_break: Some(ContextWordBreakOptions {
890                word_break: crate::width::WordBreak::BreakAll,
891                limit: NonZeroUsize::new(5).unwrap(),
892            }),
893            ..Default::default()
894        };
895
896        assert_eq!(
897            ctx.process_with_options(String::from("abcdefgh"), &opts),
898            Ok(String::from("abcde"))
899        );
900
901        assert_eq!(
902            ctx.process_with_options(String::from("a﷽"), &opts),
903            Ok(String::from("a"))
904        );
905
906        let opts = ContextProcessingOptions {
907            character_limit: Some(NonZeroUsize::new(20).unwrap()),
908            word_break: Some(ContextWordBreakOptions {
909                word_break: crate::width::WordBreak::BreakAll,
910                limit: NonZeroUsize::new(5).unwrap(),
911            }),
912            ..Default::default()
913        };
914
915        assert_eq!(
916            ctx.process_with_options("abc ௌௌௌௌ def".to_owned(), &opts),
917            Err(BlockReason::Unbroken(10))
918        );
919    }
920
921    #[test]
922    #[cfg(feature = "serde")]
923    fn serde() {
924        use std::time::SystemTime;
925
926        let mut ctx = crate::Context::default();
927        ctx.process("foo".to_string()).unwrap();
928        ctx.restrict_for(Duration::from_secs(1000));
929        println!("{}", serde_json::to_string(&ctx).unwrap());
930        let json = serde_json::to_value(&ctx).unwrap();
931        let only_safe_until = &json["only_safe_until"];
932        let unix = only_safe_until.as_i64().unwrap();
933        assert!(
934            unix > 1000
935                + SystemTime::now()
936                    .duration_since(SystemTime::UNIX_EPOCH)
937                    .unwrap()
938                    .as_millis() as i64
939        )
940    }
941}