1mod structure;
2mod utils;
3
4use std::io::Cursor;
5use std::path::PathBuf;
6
7use image::{DynamicImage, ImageReader};
8use jiff::Timestamp;
9use tokio::sync::OnceCell;
10use url::Url;
11
12use self::structure::*;
13use crate::{
14 Category, ChapterInfo, Client, Comment, CommentType, ContentInfo, ContentInfos, Error,
15 FindImageResult, FindTextResult, HTTPClient, LongComment, NovelDB, NovelInfo, Options,
16 ShortComment, Tag, UserInfo, VolumeInfo, VolumeInfos, WordCountRange,
17};
18
19#[must_use]
21pub struct SfacgClient {
22 proxy: Option<Url>,
23 no_proxy: bool,
24 cert_path: Option<PathBuf>,
25
26 client: OnceCell<HTTPClient>,
27 client_rss: OnceCell<HTTPClient>,
28
29 db: OnceCell<NovelDB>,
30}
31
32impl Client for SfacgClient {
33 fn proxy(&mut self, proxy: Url) {
34 self.proxy = Some(proxy);
35 }
36
37 fn no_proxy(&mut self) {
38 self.no_proxy = true;
39 }
40
41 fn cert(&mut self, cert_path: PathBuf) {
42 self.cert_path = Some(cert_path);
43 }
44
45 async fn shutdown(&self) -> Result<(), Error> {
46 self.client().await?.save_cookies()
47 }
48
49 async fn add_cookie(&self, cookie_str: &str, url: &Url) -> Result<(), Error> {
50 self.client().await?.add_cookie(cookie_str, url)
51 }
52
53 async fn log_in(&self, username: String, password: Option<String>) -> Result<(), Error> {
54 assert!(!username.is_empty());
55 assert!(password.is_some());
56
57 let password = password.unwrap();
58
59 let response: GenericResponse = self
60 .post("/sessions", LogInRequest { username, password })
61 .await?;
62 response.status.check()?;
63
64 Ok(())
65 }
66
67 async fn logged_in(&self) -> Result<bool, Error> {
68 let response: GenericResponse = self.get("/user").await?;
69
70 if response.status.unauthorized() {
71 Ok(false)
72 } else {
73 response.status.check()?;
74 Ok(true)
75 }
76 }
77
78 async fn user_info(&self) -> Result<UserInfo, Error> {
79 let response: UserInfoResponse = self.get("/user").await?;
80 response.status.check()?;
81 let data = response.data.unwrap();
82
83 Ok(UserInfo {
84 nickname: data.nick_name.trim().to_string(),
85 avatar: Some(data.avatar),
86 })
87 }
88
89 async fn money(&self) -> Result<u32, Error> {
90 let response: MoneyResponse = self.get("/user/money").await?;
91 response.status.check()?;
92 let data = response.data.unwrap();
93
94 Ok(data.fire_money_remain + data.coupons_remain)
95 }
96
97 async fn sign_in(&self) -> Result<(), Error> {
98 let now = Timestamp::now().in_tz(crate::TIME_ZONE_NAME)?;
99
100 let response: GenericResponse = self
101 .put(
102 "/user/newSignInfo",
103 SignRequest {
104 sign_date: now.strftime("%Y-%m-%d").to_string(),
105 },
106 )
107 .await?;
108 if response.status.already_signed_in() {
109 tracing::info!("{}", response.status.msg.unwrap().trim())
110 } else {
111 response.status.check()?;
112 }
113
114 Ok(())
115 }
116
117 async fn bookshelf_infos(&self) -> Result<Vec<u32>, Error> {
118 let response: BookshelfInfoResponse = self
119 .get_query("/user/Pockets", BookshelfInfoRequest { expand: "novels" })
120 .await?;
121 response.status.check()?;
122 let data = response.data.unwrap();
123
124 let mut result = Vec::with_capacity(32);
125 for info in data {
126 if let Some(expand) = info.expand {
127 let novels = expand.novels;
128
129 if let Some(novels) = novels {
130 for novel_info in novels {
131 result.push(novel_info.novel_id);
132 }
133 }
134 }
135 }
136
137 Ok(result)
138 }
139
140 async fn novel_info(&self, id: u32) -> Result<Option<NovelInfo>, Error> {
141 assert!(id > 0 && id <= i32::MAX as u32);
142
143 let response: NovelInfoResponse = self
144 .get_query(
145 format!("/novels/{id}"),
146 NovelInfoRequest {
147 expand: "intro,typeName,sysTags",
148 },
149 )
150 .await?;
151 if response.status.not_found() {
152 return Ok(None);
153 }
154 response.status.check()?;
155 let data = response.data.unwrap();
156
157 let category = Category {
158 id: Some(data.type_id),
159 parent_id: None,
160 name: data.expand.type_name.trim().to_string(),
161 };
162
163 let novel_info = NovelInfo {
164 id,
165 name: data.novel_name.trim().to_string(),
166 author_name: data.author_name.trim().to_string(),
167 cover_url: Some(data.novel_cover),
168 introduction: super::parse_multi_line(data.expand.intro),
169 word_count: SfacgClient::parse_word_count(data.char_count),
170 is_vip: Some(data.sign_status == "VIP"),
171 is_finished: Some(data.is_finish),
172 create_time: Some(data.add_time),
173 update_time: Some(data.last_update_time),
174 category: Some(category),
175 tags: self.parse_tags(data.expand.sys_tags).await?,
176 };
177
178 Ok(Some(novel_info))
179 }
180
181 async fn comments(
182 &self,
183 id: u32,
184 comment_type: CommentType,
185 need_replies: bool,
186 page: u16,
187 size: u16,
188 ) -> Result<Option<Vec<Comment>>, Error> {
189 assert!(id <= i32::MAX as u32);
190
191 match comment_type {
192 CommentType::Short => self.do_short_comments(id, need_replies, page, size).await,
193 CommentType::Long => self.do_long_comments(id, need_replies, page, size).await,
194 }
195 }
196
197 async fn volume_infos(&self, id: u32) -> Result<Option<VolumeInfos>, Error> {
198 assert!(id <= i32::MAX as u32);
199
200 let response: VolumeInfosResponse = self.get(format!("/novels/{id}/dirs")).await?;
201
202 if response.status.not_available() {
203 return Ok(None);
204 }
205
206 response.status.check()?;
207 let data = response.data.unwrap();
208
209 let mut volumes = VolumeInfos::with_capacity(8);
210 for volume in data.volume_list {
211 let mut volume_info = VolumeInfo {
212 id: volume.volume_id,
213 title: volume.title.trim().to_string(),
214 chapter_infos: Vec::with_capacity(volume.chapter_list.len()),
215 };
216
217 for chapter in volume.chapter_list {
218 let chapter_info = ChapterInfo {
219 novel_id: Some(chapter.novel_id),
220 id: chapter.chap_id,
221 title: chapter.title.trim().to_string(),
222 word_count: Some(chapter.char_count),
223 create_time: Some(chapter.add_time),
224 update_time: chapter.update_time,
225 is_vip: Some(chapter.is_vip),
226 price: Some(chapter.need_fire_money),
227 payment_required: Some(chapter.need_fire_money != 0),
228 is_valid: None,
229 };
230
231 volume_info.chapter_infos.push(chapter_info);
232 }
233
234 volumes.push(volume_info);
235 }
236
237 Ok(Some(volumes))
238 }
239
240 async fn content_infos(&self, info: &ChapterInfo) -> Result<ContentInfos, Error> {
241 let content;
242
243 match self.db().await?.find_text(info).await? {
244 FindTextResult::Ok(str) => {
245 content = str;
246 }
247 other => {
248 let response: ContentInfosResponse = self
249 .get_query(
250 format!("/Chaps/{}", info.id),
251 ContentInfosRequest {
252 expand: "content,isContentEncrypted",
253 },
254 )
255 .await?;
256 response.status.check()?;
257 let data = response.data.unwrap();
258
259 if data.expand.is_content_encrypted {
260 content = SfacgClient::convert(data.expand.content);
261 } else {
262 content = data.expand.content;
263 }
264
265 if content.trim().is_empty() {
266 return Err(Error::NovelApi(String::from("Content is empty")));
267 }
268
269 match other {
270 FindTextResult::None => self.db().await?.insert_text(info, &content).await?,
271 FindTextResult::Outdate => self.db().await?.update_text(info, &content).await?,
272 FindTextResult::Ok(_) => (),
273 }
274 }
275 }
276
277 let mut content_infos = ContentInfos::with_capacity(128);
278 for line in content
279 .lines()
280 .map(|line| line.trim())
281 .filter(|line| !line.is_empty())
282 {
283 if line.starts_with("[img") {
284 match SfacgClient::parse_image_url(line) {
285 Ok(url) => content_infos.push(ContentInfo::Image(url)),
286 Err(err) => tracing::error!("{err}"),
287 }
288 } else {
289 content_infos.push(ContentInfo::Text(line.to_string()));
290 }
291 }
292
293 Ok(content_infos)
294 }
295
296 async fn content_infos_multiple(
297 &self,
298 infos: &[ChapterInfo],
299 ) -> Result<Vec<ContentInfos>, Error> {
300 let mut result = Vec::new();
301
302 for info in infos {
303 result.push(self.content_infos(info).await?);
304 }
305
306 Ok(result)
307 }
308
309 async fn order_chapter(&self, info: &ChapterInfo) -> Result<(), Error> {
310 let response: GenericResponse = self
311 .post(
312 &format!("/novels/{}/orderedchaps", info.novel_id.unwrap()),
313 OrderRequest {
314 order_all: false,
315 auto_order: false,
316 chap_ids: vec![info.id],
317 order_type: "readOrder",
318 },
319 )
320 .await?;
321 if response.status.already_ordered() {
322 tracing::info!("{}", response.status.msg.unwrap().trim())
323 } else {
324 response.status.check()?;
325 }
326
327 Ok(())
328 }
329
330 async fn order_novel(&self, id: u32, _: &VolumeInfos) -> Result<(), Error> {
331 assert!(id > 0 && id <= i32::MAX as u32);
332
333 let response: GenericResponse = self
334 .post(
335 &format!("/novels/{id}/orderedchaps",),
336 OrderRequest {
337 order_all: true,
338 auto_order: false,
339 chap_ids: vec![],
340 order_type: "readOrder",
341 },
342 )
343 .await?;
344 if response.status.already_ordered() {
345 tracing::info!("{}", response.status.msg.unwrap().trim())
346 } else {
347 response.status.check()?;
348 }
349
350 Ok(())
351 }
352
353 async fn image(&self, url: &Url) -> Result<DynamicImage, Error> {
354 match self.db().await?.find_image(url).await? {
355 FindImageResult::Ok(image) => Ok(image),
356 FindImageResult::None => {
357 let response = self.get_rss(url).await?;
358 let bytes = response.bytes().await?;
359
360 let image = ImageReader::new(Cursor::new(&bytes))
361 .with_guessed_format()?
362 .decode()?;
363
364 self.db().await?.insert_image(url, bytes).await?;
365
366 Ok(image)
367 }
368 }
369 }
370
371 async fn categories(&self) -> Result<&Vec<Category>, Error> {
372 static CATEGORIES: OnceCell<Vec<Category>> = OnceCell::const_new();
373
374 CATEGORIES
375 .get_or_try_init(|| async {
376 let response: CategoryResponse = self.get("/noveltypes").await?;
377 response.status.check()?;
378 let data = response.data.unwrap();
379
380 let mut result = Vec::with_capacity(8);
381 for tag_data in data {
382 result.push(Category {
383 id: Some(tag_data.type_id),
384 parent_id: None,
385 name: tag_data.type_name.trim().to_string(),
386 });
387 }
388
389 result.sort_unstable_by_key(|x| x.id.unwrap());
390
391 Ok(result)
392 })
393 .await
394 }
395
396 async fn tags(&self) -> Result<&Vec<Tag>, Error> {
397 static TAGS: OnceCell<Vec<Tag>> = OnceCell::const_new();
398
399 TAGS.get_or_try_init(|| async {
400 let response: TagResponse = self.get("/novels/0/sysTags").await?;
401 response.status.check()?;
402 let data = response.data.unwrap();
403
404 let mut result = Vec::with_capacity(64);
405 for tag_data in data {
406 result.push(Tag {
407 id: Some(tag_data.sys_tag_id),
408 name: tag_data.tag_name.trim().to_string(),
409 });
410 }
411
412 result.push(Tag {
414 id: Some(74),
415 name: "百合".to_string(),
416 });
417
418 result.sort_unstable_by_key(|x| x.id.unwrap());
419
420 Ok(result)
421 })
422 .await
423 }
424
425 async fn search_infos(
426 &self,
427 option: &Options,
428 page: u16,
429 size: u16,
430 ) -> Result<Option<Vec<u32>>, Error> {
431 assert!(size <= 50, "The maximum number of items per page is 50");
432
433 if option.keyword.is_some() {
434 self.do_search_with_keyword(option, page, size).await
435 } else {
436 self.do_search_without_keyword(option, page, size).await
437 }
438 }
439
440 fn has_this_type_of_comments(comment_type: CommentType) -> bool {
441 match comment_type {
442 CommentType::Short => true,
443 CommentType::Long => true,
444 }
445 }
446}
447
448impl SfacgClient {
449 async fn do_short_comments(
450 &self,
451 id: u32,
452 need_replies: bool,
453 page: u16,
454 size: u16,
455 ) -> Result<Option<Vec<Comment>>, Error> {
456 assert!(size <= 50);
457
458 let response: CommentResponse = self
459 .get_query(
460 format!("/novels/{id}/Cmts"),
461 ShortCommentRequest {
462 page,
463 size,
464 r#type: "clear",
465 sort: "smart",
466 },
467 )
468 .await?;
469 response.status.check()?;
470 let data = response.data.unwrap();
471
472 if data.is_empty() {
473 return Ok(None);
474 }
475
476 let mut result = Vec::with_capacity(data.len());
477
478 for comment in data {
479 let Some(content) = super::parse_multi_line(comment.content) else {
480 continue;
481 };
482
483 result.push(Comment::Short(ShortComment {
484 id: comment.comment_id,
485 user: UserInfo {
486 nickname: comment.display_name.trim().to_string(),
487 avatar: Some(comment.avatar),
488 },
489 content,
490 create_time: Some(comment.create_time),
491 like_count: Some(comment.fav_count),
492 replies: if need_replies && comment.reply_num > 0 {
493 self.comment_replies(comment.comment_id, CommentType::Short, comment.reply_num)
494 .await?
495 } else {
496 None
497 },
498 }));
499 }
500
501 Ok(Some(result))
502 }
503
504 async fn do_long_comments(
505 &self,
506 id: u32,
507 need_replies: bool,
508 page: u16,
509 size: u16,
510 ) -> Result<Option<Vec<Comment>>, Error> {
511 assert!(size <= 20);
512
513 let response: CommentResponse = self
514 .get_query(
515 format!("/novels/{id}/lcmts"),
516 LongCommentRequest {
517 page,
518 size,
519 charlen: 140,
520 sort: "addtime",
521 },
522 )
523 .await?;
524 response.status.check()?;
525 let data = response.data.unwrap();
526
527 if data.is_empty() {
528 return Ok(None);
529 }
530
531 let mut result = Vec::with_capacity(data.len());
532
533 for comment in data {
534 result.push(Comment::Long(LongComment {
535 id: comment.comment_id,
536 user: UserInfo {
537 nickname: comment.display_name.trim().to_string(),
538 avatar: Some(comment.avatar),
539 },
540 title: comment.title.unwrap().trim().to_string(),
541 content: self.long_comment_content(comment.comment_id).await?,
542 create_time: Some(comment.create_time),
543 like_count: Some(comment.fav_count),
544 replies: if need_replies && comment.reply_num > 0 {
545 self.comment_replies(comment.comment_id, CommentType::Long, comment.reply_num)
546 .await?
547 } else {
548 None
549 },
550 }));
551 }
552
553 Ok(Some(result))
554 }
555
556 async fn long_comment_content(&self, comment_id: u32) -> Result<Vec<String>, Error> {
557 let response: LongCommentContentResponse = self.get(format!("/lcmts/{comment_id}")).await?;
558 response.status.check()?;
559 let data = response.data.unwrap();
560
561 Ok(super::parse_multi_line(data.content).unwrap())
562 }
563
564 async fn comment_replies(
565 &self,
566 comment_id: u32,
567 comment_type: CommentType,
568 total: u16,
569 ) -> Result<Option<Vec<ShortComment>>, Error> {
570 let url = match comment_type {
571 CommentType::Short => format!("/cmts/{comment_id}/replys"),
572 CommentType::Long => format!("/lcmts/{comment_id}/replys"),
573 };
574
575 let mut page = 0;
576 let size = 50;
577 let total_page = if total.is_multiple_of(size) {
578 total / size
579 } else {
580 total / size + 1
581 };
582 let mut reply_list = Vec::with_capacity(total as usize);
583
584 while page < total_page {
585 let response: ReplyResponse = self.get_query(&url, ReplyRequest { page, size }).await?;
586 response.status.check()?;
587 let data = response.data.unwrap();
588
589 for reply in data {
590 let Some(content) = super::parse_multi_line(reply.content) else {
591 continue;
592 };
593
594 reply_list.push(ShortComment {
595 id: reply.reply_id,
596 user: UserInfo {
597 nickname: reply.display_name.trim().to_string(),
598 avatar: Some(reply.avatar),
599 },
600 content,
601 create_time: Some(reply.create_time),
602 like_count: None,
603 replies: None,
604 });
605 }
606
607 page += 1;
608 }
609
610 if reply_list.is_empty() {
611 Ok(None)
612 } else {
613 reply_list.sort_unstable_by_key(|x| x.create_time.unwrap());
614 reply_list.dedup();
615 Ok(Some(reply_list))
616 }
617 }
618
619 async fn do_search_with_keyword(
620 &self,
621 option: &Options,
622 page: u16,
623 size: u16,
624 ) -> Result<Option<Vec<u32>>, Error> {
625 let is_finish = if let Some(is_finished) = option.is_finished {
629 if is_finished { 1 } else { 0 }
630 } else {
631 -1
632 };
633
634 let update_days = if let Some(update_days) = option.update_days {
636 update_days as i8
637 } else {
638 -1
639 };
640
641 let response: SearchResponse = self
642 .get_query(
643 "/search/novels/result/new",
644 SearchRequest {
645 q: option.keyword.as_ref().unwrap().to_string(),
646 is_finish,
647 update_days,
648 systagids: SfacgClient::tag_ids(&option.tags),
649 page,
650 size,
651 sort: "hot",
657 expand: "sysTags",
658 },
659 )
660 .await?;
661 response.status.check()?;
662 let data = response.data.unwrap();
663
664 if data.novels.is_empty() {
665 return Ok(None);
666 }
667
668 let mut result = Vec::new();
669 let sys_tags = self.tags().await?;
670
671 for novel_info in data.novels {
672 let mut tag_ids = vec![];
673
674 for tag in novel_info.expand.sys_tags {
675 if let Some(sys_tag) = sys_tags.iter().find(|x| x.id.unwrap() == tag.sys_tag_id) {
676 tag_ids.push(sys_tag.id.unwrap());
677 }
678 }
679
680 if SfacgClient::match_category(option, novel_info.type_id)
681 && SfacgClient::match_excluded_tags(option, tag_ids)
682 && SfacgClient::match_vip(option, &novel_info.sign_status)
683 && SfacgClient::match_word_count(option, novel_info.char_count)
684 {
685 result.push(novel_info.novel_id);
686 }
687 }
688
689 Ok(Some(result))
690 }
691
692 async fn do_search_without_keyword(
693 &self,
694 option: &Options,
695 page: u16,
696 size: u16,
697 ) -> Result<Option<Vec<u32>>, Error> {
698 let mut category_id = 0;
699 if let Some(category) = &option.category {
700 category_id = category.id.unwrap();
701 }
702
703 let updatedays = if let Some(update_days) = option.update_days {
705 update_days as i8
706 } else {
707 -1
708 };
709
710 let isfinish = SfacgClient::bool_to_str(&option.is_finished);
711 let isfree = SfacgClient::bool_to_str(&option.is_vip.as_ref().map(|x| !x));
712
713 let systagids = SfacgClient::tag_ids(&option.tags);
714 let notexcludesystagids = SfacgClient::tag_ids(&option.excluded_tags);
715
716 let mut charcountbegin = 0;
717 let mut charcountend = 0;
718
719 if let Some(word_count) = &option.word_count {
720 match word_count {
721 WordCountRange::Range(range) => {
722 charcountbegin = range.start;
723 charcountend = range.end;
724 }
725 WordCountRange::RangeFrom(range_from) => charcountbegin = range_from.start,
726 WordCountRange::RangeTo(range_to) => charcountend = range_to.end,
727 }
728 }
729
730 let response: NovelsResponse = self
731 .get_query(
732 format!("/novels/{category_id}/sysTags/novels"),
733 NovelsRequest {
734 charcountbegin,
735 charcountend,
736 isfinish,
737 isfree,
738 systagids,
739 notexcludesystagids,
740 updatedays,
741 page,
742 size,
743 sort: "viewtimes",
749 },
750 )
751 .await?;
752 response.status.check()?;
753 let data = response.data.unwrap();
754
755 if data.is_empty() {
756 return Ok(None);
757 }
758
759 let mut result = Vec::new();
760 for novel_data in data {
761 result.push(novel_data.novel_id);
762 }
763
764 Ok(Some(result))
765 }
766
767 fn parse_word_count(word_count: i32) -> Option<u32> {
768 if word_count <= 0 {
770 None
771 } else {
772 Some(word_count as u32)
773 }
774 }
775
776 async fn parse_tags(&self, tag_list: Vec<NovelInfoSysTag>) -> Result<Option<Vec<Tag>>, Error> {
777 let sys_tags = self.tags().await?;
778
779 let mut result = Vec::new();
780 for tag in tag_list {
781 let id = tag.sys_tag_id;
782 let name = tag.tag_name.trim().to_string();
783
784 if sys_tags.iter().any(|sys_tag| sys_tag.id.unwrap() == id) {
786 result.push(Tag { id: Some(id), name });
787 } else {
788 tracing::info!("This tag is not a system tag and is ignored: {name}");
789 }
790 }
791
792 if result.is_empty() {
793 Ok(None)
794 } else {
795 result.sort_unstable_by_key(|x| x.id.unwrap());
796 Ok(Some(result))
797 }
798 }
799
800 fn parse_image_url(line: &str) -> Result<Url, Error> {
801 let begin = line.find("http");
802 let end = line.find("[/img]");
803
804 if begin.is_none() || end.is_none() {
805 return Err(Error::NovelApi(format!(
806 "Image URL format is incorrect: {line}"
807 )));
808 }
809
810 let begin = begin.unwrap();
811 let end = end.unwrap();
812
813 let url = line
814 .chars()
815 .skip(begin)
816 .take(end - begin)
817 .collect::<String>()
818 .trim()
819 .to_string();
820
821 match Url::parse(&url) {
822 Ok(url) => Ok(url),
823 Err(error) => Err(Error::NovelApi(format!(
824 "Image URL parse failed: {error}, content: {line}"
825 ))),
826 }
827 }
828
829 fn bool_to_str(flag: &Option<bool>) -> &'static str {
830 if flag.is_some() {
831 if *flag.as_ref().unwrap() { "is" } else { "not" }
832 } else {
833 "both"
834 }
835 }
836
837 fn tag_ids(tags: &Option<Vec<Tag>>) -> Option<String> {
838 tags.as_ref().map(|tags| {
839 tags.iter()
840 .map(|tag| tag.id.unwrap().to_string())
841 .collect::<Vec<String>>()
842 .join(",")
843 })
844 }
845
846 fn match_vip(option: &Options, sign_status: &str) -> bool {
847 if option.is_vip.is_none() {
848 return true;
849 }
850
851 if *option.is_vip.as_ref().unwrap() {
852 sign_status == "VIP"
853 } else {
854 sign_status != "VIP"
855 }
856 }
857
858 fn match_excluded_tags(option: &Options, tag_ids: Vec<u16>) -> bool {
859 if option.excluded_tags.is_none() {
860 return true;
861 }
862
863 tag_ids.iter().all(|id| {
864 !option
865 .excluded_tags
866 .as_ref()
867 .unwrap()
868 .iter()
869 .any(|tag| tag.id.unwrap() == *id)
870 })
871 }
872
873 fn match_category(option: &Options, category_id: u16) -> bool {
874 if option.category.is_none() {
875 return true;
876 }
877
878 let category = option.category.as_ref().unwrap();
879 category.id.unwrap() == category_id
880 }
881
882 fn match_word_count(option: &Options, word_count: i32) -> bool {
883 if option.word_count.is_none() {
884 return true;
885 }
886
887 if word_count <= 0 {
888 return true;
889 }
890
891 let word_count = word_count as u32;
892 match option.word_count.as_ref().unwrap() {
893 WordCountRange::Range(range) => {
894 if word_count >= range.start && word_count < range.end {
895 return true;
896 }
897 }
898 WordCountRange::RangeFrom(range_from) => {
899 if word_count >= range_from.start {
900 return true;
901 }
902 }
903 WordCountRange::RangeTo(rang_to) => {
904 if word_count < rang_to.end {
905 return true;
906 }
907 }
908 }
909
910 false
911 }
912}