tuitbot_core/scoring/
engine.rs1use chrono::{DateTime, Utc};
4
5use crate::config::ScoringConfig;
6
7use super::signals;
8use super::{TweetData, TweetScore};
9
10pub struct ScoringEngine {
12 pub(super) config: ScoringConfig,
13 pub(super) keywords: Vec<String>,
14}
15
16impl ScoringEngine {
17 pub fn new(config: ScoringConfig, keywords: Vec<String>) -> Self {
22 Self { config, keywords }
23 }
24
25 pub fn score_tweet(&self, tweet: &TweetData) -> TweetScore {
29 self.score_tweet_at(tweet, Utc::now())
30 }
31
32 pub fn score_tweet_at(&self, tweet: &TweetData, now: DateTime<Utc>) -> TweetScore {
36 let keyword_relevance = signals::keyword_relevance(
37 &tweet.text,
38 &self.keywords,
39 self.config.keyword_relevance_max,
40 );
41
42 let follower = signals::targeted_follower_score(
43 tweet.author_followers,
44 self.config.follower_count_max,
45 );
46
47 let recency = signals::recency_score_at(&tweet.created_at, self.config.recency_max, now);
48
49 let engagement = signals::engagement_rate(
50 tweet.likes,
51 tweet.retweets,
52 tweet.replies,
53 tweet.author_followers,
54 self.config.engagement_rate_max,
55 );
56
57 let reply_count = signals::reply_count_score(tweet.replies, self.config.reply_count_max);
58
59 let content_type = signals::content_type_score(
60 tweet.has_media,
61 tweet.is_quote_tweet,
62 self.config.content_type_max,
63 );
64
65 let total =
66 (keyword_relevance + follower + recency + engagement + reply_count + content_type)
67 .clamp(0.0, 100.0);
68 let meets_threshold = total >= self.config.threshold as f32;
69
70 tracing::debug!(
71 author = %tweet.author_username,
72 total = format!("{:.0}", total),
73 keyword = format!("{:.0}", keyword_relevance),
74 follower = format!("{:.0}", follower),
75 recency = format!("{:.0}", recency),
76 engagement = format!("{:.0}", engagement),
77 reply = format!("{:.0}", reply_count),
78 content = format!("{:.0}", content_type),
79 meets = meets_threshold,
80 "Scored tweet",
81 );
82
83 TweetScore {
84 total,
85 keyword_relevance,
86 follower,
87 recency,
88 engagement,
89 reply_count,
90 content_type,
91 meets_threshold,
92 }
93 }
94
95 pub fn keywords(&self) -> &[String] {
97 &self.keywords
98 }
99
100 pub fn config(&self) -> &ScoringConfig {
102 &self.config
103 }
104}