Skip to main content

xcom_rs/handlers/
timeline.rs

1use crate::{
2    cli::TimelineCommands,
3    output::{print_envelope, print_ndjson, OutputFormat},
4    protocol::{Envelope, ErrorCode, ErrorDetails, ExitCode},
5    timeline::{
6        commands::TimelineError,
7        models::{TimelineArgs, TimelineKind},
8        TimelineCommand,
9    },
10};
11use anyhow::Result;
12use std::collections::HashMap;
13
14/// Handle timeline subcommands.
15pub fn handle_timeline(
16    command: TimelineCommands,
17    create_meta: &dyn Fn() -> Option<HashMap<String, serde_json::Value>>,
18    output_format: OutputFormat,
19) -> Result<()> {
20    let timeline_cmd = TimelineCommand::new();
21
22    let (args, response_type) = match command {
23        TimelineCommands::Home { limit, cursor } => (
24            TimelineArgs {
25                kind: TimelineKind::Home,
26                limit,
27                cursor,
28            },
29            "timeline.home",
30        ),
31        TimelineCommands::Mentions { limit, cursor } => (
32            TimelineArgs {
33                kind: TimelineKind::Mentions,
34                limit,
35                cursor,
36            },
37            "timeline.mentions",
38        ),
39        TimelineCommands::User {
40            handle,
41            limit,
42            cursor,
43        } => (
44            TimelineArgs {
45                kind: TimelineKind::User { handle },
46                limit,
47                cursor,
48            },
49            "timeline.user",
50        ),
51    };
52
53    tracing::info!(response_type = %response_type, "Fetching timeline");
54
55    match timeline_cmd.get(args) {
56        Ok(result) => {
57            if output_format == OutputFormat::Ndjson {
58                print_ndjson(&result.tweets)
59            } else {
60                let envelope = if let Some(meta) = create_meta() {
61                    Envelope::success_with_meta(response_type, result, meta)
62                } else {
63                    Envelope::success(response_type, result)
64                };
65                print_envelope(&envelope, output_format)
66            }
67        }
68        Err(e) => {
69            let error = build_error_details(&e);
70
71            let envelope = if let Some(meta) = create_meta() {
72                Envelope::<()>::error_with_meta("error", error, meta)
73            } else {
74                Envelope::<()>::error("error", error)
75            };
76            let _ = print_envelope(&envelope, output_format);
77
78            let exit_code: i32 = match &e {
79                TimelineError::AuthRequired => ExitCode::AuthenticationError.into(),
80                TimelineError::ApiError(_) => ExitCode::OperationFailed.into(),
81            };
82            std::process::exit(exit_code);
83        }
84    }
85}
86
87fn build_error_details(e: &TimelineError) -> ErrorDetails {
88    match e {
89        TimelineError::AuthRequired => ErrorDetails::auth_required(
90            e.to_string(),
91            vec!["Run 'xcom-rs auth login' to authenticate.".to_string()],
92        ),
93        TimelineError::ApiError(classified) => {
94            if let Some(retry_after_ms) = classified.retry_after_ms {
95                ErrorDetails::with_retry_after(e.to_error_code(), e.to_string(), retry_after_ms)
96            } else {
97                ErrorDetails::new(e.to_error_code(), e.to_string())
98            }
99        }
100    }
101}
102
103/// Convenience: build ErrorCode from TimelineError (for use in other modules)
104impl From<&TimelineError> for ErrorCode {
105    fn from(e: &TimelineError) -> Self {
106        e.to_error_code()
107    }
108}