Skip to main content

threads_rs/api/
insights.rs

1use std::collections::HashMap;
2
3use crate::client::Client;
4use crate::constants;
5use crate::error;
6use crate::types::{AccountInsightsOptions, InsightsResponse, PostId, PostInsightsOptions, UserId};
7
8/// Returns available post insight metric names as string slices.
9pub fn get_available_post_insight_metrics() -> Vec<&'static str> {
10    vec!["views", "likes", "replies", "reposts", "quotes", "shares"]
11}
12
13/// Returns available account insight metric names as string slices.
14pub fn get_available_account_insight_metrics() -> Vec<&'static str> {
15    vec![
16        "views",
17        "likes",
18        "replies",
19        "reposts",
20        "quotes",
21        "clicks",
22        "followers_count",
23        "follower_demographics",
24    ]
25}
26
27/// Returns available insight period names as string slices.
28pub fn get_available_insight_periods() -> Vec<&'static str> {
29    vec!["day", "lifetime"]
30}
31
32/// Returns available follower demographics breakdown names as string slices.
33pub fn get_available_follower_demographics_breakdowns() -> Vec<&'static str> {
34    vec!["country", "city", "age", "gender"]
35}
36
37impl Client {
38    /// Get insights for a specific post.
39    pub async fn get_post_insights(
40        &self,
41        post_id: &PostId,
42        metrics: &[&str],
43    ) -> crate::Result<InsightsResponse> {
44        if !post_id.is_valid() {
45            return Err(error::new_validation_error(
46                0,
47                constants::ERR_EMPTY_POST_ID,
48                "",
49                "post_id",
50            ));
51        }
52
53        let token = self.access_token().await;
54        let mut params = HashMap::new();
55
56        let metric_str = if metrics.is_empty() {
57            "views,likes,replies,reposts,quotes".to_owned()
58        } else {
59            metrics.join(",")
60        };
61        params.insert("metric".into(), metric_str);
62
63        let path = format!("/{}/insights", post_id);
64        let resp = self.http_client.get(&path, params, &token).await?;
65        resp.json()
66    }
67
68    /// Get insights for a user account.
69    pub async fn get_account_insights(
70        &self,
71        user_id: &UserId,
72        metrics: &[&str],
73        period: &str,
74    ) -> crate::Result<InsightsResponse> {
75        if !user_id.is_valid() {
76            return Err(error::new_validation_error(
77                0,
78                constants::ERR_EMPTY_USER_ID,
79                "",
80                "user_id",
81            ));
82        }
83
84        let token = self.access_token().await;
85        let mut params = HashMap::new();
86
87        let metric_str = if metrics.is_empty() {
88            "views,likes,replies,reposts,quotes,followers_count".to_owned()
89        } else {
90            metrics.join(",")
91        };
92        params.insert("metric".into(), metric_str);
93
94        let period_str = if period.is_empty() {
95            "lifetime".to_owned()
96        } else {
97            period.to_owned()
98        };
99        params.insert("period".into(), period_str);
100
101        let path = format!("/{}/threads_insights", user_id);
102        let resp = self.http_client.get(&path, params, &token).await?;
103        resp.json()
104    }
105
106    /// Get post insights with typed options.
107    pub async fn get_post_insights_with_options(
108        &self,
109        post_id: &PostId,
110        opts: &PostInsightsOptions,
111    ) -> crate::Result<InsightsResponse> {
112        if !post_id.is_valid() {
113            return Err(error::new_validation_error(
114                0,
115                constants::ERR_EMPTY_POST_ID,
116                "",
117                "post_id",
118            ));
119        }
120
121        let token = self.access_token().await;
122        let mut params = HashMap::new();
123
124        let metric_str = match &opts.metrics {
125            Some(metrics) if !metrics.is_empty() => metrics
126                .iter()
127                .map(|m| {
128                    serde_json::to_string(m)
129                        .unwrap_or_default()
130                        .trim_matches('"')
131                        .to_owned()
132                })
133                .collect::<Vec<_>>()
134                .join(","),
135            _ => "views,likes,replies,reposts,quotes".to_owned(),
136        };
137        params.insert("metric".into(), metric_str);
138
139        // Note: `period` is not a documented request parameter for post insights,
140        // it only appears in the response data. We intentionally omit it here.
141
142        if let Some(since) = opts.since {
143            params.insert("since".into(), since.timestamp().to_string());
144        }
145        if let Some(until) = opts.until {
146            params.insert("until".into(), until.timestamp().to_string());
147        }
148
149        let path = format!("/{}/insights", post_id);
150        let resp = self.http_client.get(&path, params, &token).await?;
151        resp.json()
152    }
153
154    /// Get account insights with typed options.
155    pub async fn get_account_insights_with_options(
156        &self,
157        user_id: &UserId,
158        opts: &AccountInsightsOptions,
159    ) -> crate::Result<InsightsResponse> {
160        if !user_id.is_valid() {
161            return Err(error::new_validation_error(
162                0,
163                constants::ERR_EMPTY_USER_ID,
164                "",
165                "user_id",
166            ));
167        }
168
169        let token = self.access_token().await;
170        let mut params = HashMap::new();
171
172        let metric_str = match &opts.metrics {
173            Some(metrics) if !metrics.is_empty() => metrics
174                .iter()
175                .map(|m| {
176                    serde_json::to_string(m)
177                        .unwrap_or_default()
178                        .trim_matches('"')
179                        .to_owned()
180                })
181                .collect::<Vec<_>>()
182                .join(","),
183            _ => "views,likes,replies,reposts,quotes,followers_count".to_owned(),
184        };
185        params.insert("metric".into(), metric_str);
186
187        let period_str = match &opts.period {
188            Some(period) => serde_json::to_string(period)
189                .unwrap_or_default()
190                .trim_matches('"')
191                .to_owned(),
192            None => "lifetime".to_owned(),
193        };
194        params.insert("period".into(), period_str);
195
196        if let Some(since) = opts.since {
197            params.insert("since".into(), since.timestamp().to_string());
198        }
199        if let Some(until) = opts.until {
200            params.insert("until".into(), until.timestamp().to_string());
201        }
202        if let Some(ref breakdown) = opts.breakdown {
203            params.insert(
204                "breakdown".into(),
205                serde_json::to_string(breakdown)
206                    .unwrap_or_default()
207                    .trim_matches('"')
208                    .to_owned(),
209            );
210        }
211
212        let path = format!("/{}/threads_insights", user_id);
213        let resp = self.http_client.get(&path, params, &token).await?;
214        resp.json()
215    }
216}