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