Skip to main content

tuitbot_core/automation/posting_queue/
queue.rs

1//! Serialized posting queue for concurrent automation loops.
2//!
3//! All loops funnel post actions through a single bounded MPSC channel,
4//! preventing race conditions and ensuring rate limits are respected
5//! globally. A single consumer task processes actions sequentially with
6//! configurable delays between posts.
7
8use tokio::sync::{mpsc, oneshot};
9
10/// Default bounded channel capacity for the posting queue.
11pub const QUEUE_CAPACITY: usize = 100;
12
13/// An action to be executed by the posting queue consumer.
14///
15/// Each variant optionally includes a oneshot sender so the caller can
16/// await the result (e.g., the posted tweet ID or an error message).
17pub enum PostAction {
18    /// Reply to an existing tweet.
19    Reply {
20        /// The ID of the tweet to reply to.
21        tweet_id: String,
22        /// The reply content.
23        content: String,
24        /// Media IDs to attach (already uploaded to X API).
25        media_ids: Vec<String>,
26        /// Optional channel to receive the result (posted tweet ID or error).
27        result_tx: Option<oneshot::Sender<Result<String, String>>>,
28    },
29    /// Post a new original tweet.
30    Tweet {
31        /// The tweet content.
32        content: String,
33        /// Media IDs to attach (already uploaded to X API).
34        media_ids: Vec<String>,
35        /// Optional channel to receive the result.
36        result_tx: Option<oneshot::Sender<Result<String, String>>>,
37    },
38    /// Post a tweet as part of a thread (reply to previous tweet in thread).
39    ThreadTweet {
40        /// The tweet content.
41        content: String,
42        /// The ID of the previous tweet in the thread.
43        in_reply_to: String,
44        /// Media IDs to attach (already uploaded to X API).
45        media_ids: Vec<String>,
46        /// Optional channel to receive the result.
47        result_tx: Option<oneshot::Sender<Result<String, String>>>,
48    },
49}
50
51impl std::fmt::Debug for PostAction {
52    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
53        match self {
54            PostAction::Reply {
55                tweet_id,
56                content,
57                media_ids,
58                ..
59            } => f
60                .debug_struct("Reply")
61                .field("tweet_id", tweet_id)
62                .field("content_len", &content.len())
63                .field("media_count", &media_ids.len())
64                .finish(),
65            PostAction::Tweet {
66                content, media_ids, ..
67            } => f
68                .debug_struct("Tweet")
69                .field("content_len", &content.len())
70                .field("media_count", &media_ids.len())
71                .finish(),
72            PostAction::ThreadTweet {
73                content,
74                in_reply_to,
75                media_ids,
76                ..
77            } => f
78                .debug_struct("ThreadTweet")
79                .field("in_reply_to", in_reply_to)
80                .field("content_len", &content.len())
81                .field("media_count", &media_ids.len())
82                .finish(),
83        }
84    }
85}
86
87/// Trait for executing post actions against the X API.
88///
89/// This trait decouples the posting queue from the actual API client,
90/// allowing the queue to be tested with mock executors.
91#[async_trait::async_trait]
92pub trait PostExecutor: Send + Sync {
93    /// Post a reply to a specific tweet. Returns the posted tweet ID.
94    async fn execute_reply(
95        &self,
96        tweet_id: &str,
97        content: &str,
98        media_ids: &[String],
99    ) -> Result<String, String>;
100
101    /// Post a new original tweet. Returns the posted tweet ID.
102    async fn execute_tweet(&self, content: &str, media_ids: &[String]) -> Result<String, String>;
103}
104
105/// Create a bounded posting queue channel.
106///
107/// Returns `(sender, receiver)`. Clone the sender for each automation loop.
108/// Pass the receiver to [`run_posting_queue`].
109pub fn create_posting_queue() -> (mpsc::Sender<PostAction>, mpsc::Receiver<PostAction>) {
110    mpsc::channel(QUEUE_CAPACITY)
111}
112
113/// Trait for queueing actions for human approval instead of posting.
114#[async_trait::async_trait]
115pub trait ApprovalQueue: Send + Sync {
116    /// Queue a reply for human review. Returns the queue item ID.
117    async fn queue_reply(
118        &self,
119        tweet_id: &str,
120        content: &str,
121        media_paths: &[String],
122    ) -> Result<i64, String>;
123
124    /// Queue a tweet for human review. Returns the queue item ID.
125    async fn queue_tweet(&self, content: &str, media_paths: &[String]) -> Result<i64, String>;
126}