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#[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 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#[derive(Clone, Debug)]
51#[cfg_attr(doc, doc(cfg(feature = "context")))]
52pub struct ContextProcessingOptions {
53 pub block_if_muted: bool,
55 pub block_if_empty: bool,
57 pub block_if_severely_inappropriate: bool,
59 pub safe_mode_until: Option<Instant>,
62 pub character_limit: Option<NonZeroUsize>,
66 #[cfg(feature = "width")]
68 pub word_break: Option<ContextWordBreakOptions>,
69 pub rate_limit: Option<ContextRateLimitOptions>,
71 pub repetition_limit: Option<ContextRepetitionLimitOptions>,
73 pub max_safe_timeout: Duration,
76 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#[derive(Clone, Debug)]
100#[cfg_attr(doc, doc(cfg(feature = "context")))]
101pub struct ContextRateLimitOptions {
102 pub limit: Duration,
104 pub burst: u8,
106 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 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#[derive(Clone, Debug)]
137#[cfg(feature = "width")]
138#[cfg_attr(doc, doc(cfg(all(feature = "context", feature = "width"))))]
139pub struct ContextWordBreakOptions {
140 pub word_break: crate::width::WordBreak,
142 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#[derive(Clone, Debug)]
158#[cfg_attr(doc, doc(cfg(feature = "context")))]
159pub struct ContextRepetitionLimitOptions {
160 pub limit: u8,
162 pub memory: Duration,
164 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 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 pub fn process(&mut self, message: String) -> Result<String, BlockReason> {
212 self.process_with_options(message, &ContextProcessingOptions::default())
213 }
214
215 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 let is_kinda_sus = suspicion >= 2;
234 let is_impostor = suspicion >= 15;
235
236 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 let censor_first_character_threshold = if is_kinda_sus {
250 censor_threshold
251 } else {
252 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 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 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 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 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 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 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 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 pub fn last_message(&self) -> Option<Instant> {
463 self.last_message.map(|t| t.0)
464 }
465
466 pub fn muted_until(&self) -> Option<Instant> {
468 self.muted_until.map(|t| t.0)
469 }
470
471 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 pub fn restricted_until(&self) -> Option<Instant> {
480 self.only_safe_until.map(|t| t.0)
481 }
482
483 pub fn mute_for(&mut self, duration: Duration) {
486 self.mute_until(Instant::now() + duration);
487 }
488
489 pub fn mute_until(&mut self, instant: Instant) {
492 self.muted_until = Some(Time(instant));
493 }
494
495 pub fn restrict_for(&mut self, duration: Duration) {
498 self.restrict_until(Instant::now() + duration);
499 }
500
501 pub fn restrict_until(&mut self, instant: Instant) {
504 self.only_safe_until = Some(Time(instant));
505 }
506
507 pub fn report(&mut self) {
510 self.reports = self.reports.saturating_add(1);
511 }
512
513 pub fn reports(&self) -> usize {
516 self.reports as usize
517 }
518
519 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 pub fn total(&self) -> usize {
530 self.total as usize
531 }
532
533 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#[derive(Copy, Clone, Debug, PartialEq)]
549#[non_exhaustive]
550#[cfg_attr(doc, doc(cfg(feature = "context")))]
551pub enum BlockReason {
552 Inappropriate(Type),
554 #[cfg(feature = "width")]
555 Unbroken(usize),
557 Unsafe {
561 remaining: Duration,
562 targeted: bool,
564 },
565 Repetitious(usize),
567 Spam(Duration),
569 Muted(Duration),
571 Empty,
573}
574
575impl BlockReason {
576 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 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}