atrium_cli/
runner.rs

1use crate::commands::Command;
2use crate::store::SimpleJsonFileSessionStore;
3use anyhow::{Context, Result};
4use atrium_api::agent::bluesky::{AtprotoServiceType, BSKY_CHAT_DID};
5use atrium_api::agent::{store::SessionStore, AtpAgent};
6use atrium_api::records::{KnownRecord, Record};
7use atrium_api::types::string::{AtIdentifier, Datetime, Handle};
8use atrium_api::types::LimitedNonZeroU8;
9use atrium_xrpc_client::reqwest::ReqwestClient;
10use serde::Serialize;
11use std::ffi::OsStr;
12use std::path::PathBuf;
13use tokio::fs::{create_dir_all, File};
14use tokio::io::AsyncReadExt;
15
16pub struct Runner {
17    agent: AtpAgent<SimpleJsonFileSessionStore, ReqwestClient>,
18    limit: LimitedNonZeroU8<100>,
19    debug: bool,
20    session_path: PathBuf,
21    handle: Option<Handle>,
22}
23
24impl Runner {
25    pub async fn new(pds_host: String, limit: LimitedNonZeroU8<100>, debug: bool) -> Result<Self> {
26        let config_dir = dirs::config_dir()
27            .with_context(|| format!("No config dir: {:?}", dirs::config_dir()))?;
28        let dir = config_dir.join("atrium-cli");
29        create_dir_all(&dir).await?;
30        let session_path = dir.join("session.json");
31        let store = SimpleJsonFileSessionStore::new(session_path.clone());
32        let session = store.get_session().await;
33        let handle = session.as_ref().map(|s| s.handle.clone());
34        let agent = AtpAgent::new(ReqwestClient::new(pds_host), store);
35        if let Some(s) = &session {
36            agent.resume_session(s.clone()).await?;
37        }
38        Ok(Self {
39            agent,
40            limit,
41            debug,
42            session_path,
43            handle,
44        })
45    }
46    pub async fn run(&self, command: Command) -> Result<()> {
47        let limit = self.limit;
48        match command {
49            Command::Login(args) => {
50                self.agent.login(args.identifier, args.password).await?;
51                println!("Login successful! Saved session to {:?}", self.session_path);
52                Ok(())
53            }
54            Command::GetTimeline => self.print(
55                &self
56                    .agent
57                    .api
58                    .app
59                    .bsky
60                    .feed
61                    .get_timeline(
62                        atrium_api::app::bsky::feed::get_timeline::ParametersData {
63                            algorithm: None,
64                            cursor: None,
65                            limit: Some(limit),
66                        }
67                        .into(),
68                    )
69                    .await?,
70            ),
71            Command::GetAuthorFeed(args) => self.print(
72                &self
73                    .agent
74                    .api
75                    .app
76                    .bsky
77                    .feed
78                    .get_author_feed(
79                        atrium_api::app::bsky::feed::get_author_feed::ParametersData {
80                            actor: args
81                                .actor
82                                .or(self.handle.clone().map(AtIdentifier::Handle))
83                                .with_context(|| "Not logged in")?,
84                            cursor: None,
85                            filter: None,
86                            limit: Some(limit),
87                        }
88                        .into(),
89                    )
90                    .await?,
91            ),
92            Command::GetLikes(args) => self.print(
93                &self
94                    .agent
95                    .api
96                    .app
97                    .bsky
98                    .feed
99                    .get_likes(
100                        atrium_api::app::bsky::feed::get_likes::ParametersData {
101                            cid: None,
102                            cursor: None,
103                            limit: Some(limit),
104                            uri: args.uri.to_string(),
105                        }
106                        .into(),
107                    )
108                    .await?,
109            ),
110            Command::GetRepostedBy(args) => self.print(
111                &self
112                    .agent
113                    .api
114                    .app
115                    .bsky
116                    .feed
117                    .get_reposted_by(
118                        atrium_api::app::bsky::feed::get_reposted_by::ParametersData {
119                            cid: None,
120                            cursor: None,
121                            limit: Some(limit),
122                            uri: args.uri.to_string(),
123                        }
124                        .into(),
125                    )
126                    .await?,
127            ),
128            Command::GetActorFeeds(args) => self.print(
129                &self
130                    .agent
131                    .api
132                    .app
133                    .bsky
134                    .feed
135                    .get_actor_feeds(
136                        atrium_api::app::bsky::feed::get_actor_feeds::ParametersData {
137                            actor: args
138                                .actor
139                                .or(self.handle.clone().map(AtIdentifier::Handle))
140                                .with_context(|| "Not logged in")?,
141                            cursor: None,
142                            limit: Some(limit),
143                        }
144                        .into(),
145                    )
146                    .await?,
147            ),
148            Command::GetFeed(args) => self.print(
149                &self
150                    .agent
151                    .api
152                    .app
153                    .bsky
154                    .feed
155                    .get_feed(
156                        atrium_api::app::bsky::feed::get_feed::ParametersData {
157                            cursor: None,
158                            feed: args.uri.to_string(),
159                            limit: Some(limit),
160                        }
161                        .into(),
162                    )
163                    .await?,
164            ),
165            Command::GetListFeed(args) => self.print(
166                &self
167                    .agent
168                    .api
169                    .app
170                    .bsky
171                    .feed
172                    .get_list_feed(
173                        atrium_api::app::bsky::feed::get_list_feed::ParametersData {
174                            cursor: None,
175                            limit: Some(limit),
176                            list: args.uri.to_string(),
177                        }
178                        .into(),
179                    )
180                    .await?,
181            ),
182            Command::GetFollows(args) => self.print(
183                &self
184                    .agent
185                    .api
186                    .app
187                    .bsky
188                    .graph
189                    .get_follows(
190                        atrium_api::app::bsky::graph::get_follows::ParametersData {
191                            actor: args
192                                .actor
193                                .or(self.handle.clone().map(AtIdentifier::Handle))
194                                .with_context(|| "Not logged in")?,
195                            cursor: None,
196                            limit: Some(limit),
197                        }
198                        .into(),
199                    )
200                    .await?,
201            ),
202            Command::GetFollowers(args) => self.print(
203                &self
204                    .agent
205                    .api
206                    .app
207                    .bsky
208                    .graph
209                    .get_followers(
210                        atrium_api::app::bsky::graph::get_followers::ParametersData {
211                            actor: args
212                                .actor
213                                .or(self.handle.clone().map(AtIdentifier::Handle))
214                                .with_context(|| "Not logged in")?,
215                            cursor: None,
216                            limit: Some(limit),
217                        }
218                        .into(),
219                    )
220                    .await?,
221            ),
222            Command::GetLists(args) => self.print(
223                &self
224                    .agent
225                    .api
226                    .app
227                    .bsky
228                    .graph
229                    .get_lists(
230                        atrium_api::app::bsky::graph::get_lists::ParametersData {
231                            actor: args
232                                .actor
233                                .or(self.handle.clone().map(AtIdentifier::Handle))
234                                .with_context(|| "Not logged in")?,
235                            cursor: None,
236                            limit: Some(limit),
237                        }
238                        .into(),
239                    )
240                    .await?,
241            ),
242            Command::GetList(args) => self.print(
243                &self
244                    .agent
245                    .api
246                    .app
247                    .bsky
248                    .graph
249                    .get_list(
250                        atrium_api::app::bsky::graph::get_list::ParametersData {
251                            cursor: None,
252                            limit: Some(limit),
253                            list: args.uri.to_string(),
254                        }
255                        .into(),
256                    )
257                    .await?,
258            ),
259            Command::GetProfile(args) => self.print(
260                &self
261                    .agent
262                    .api
263                    .app
264                    .bsky
265                    .actor
266                    .get_profile(
267                        atrium_api::app::bsky::actor::get_profile::ParametersData {
268                            actor: args
269                                .actor
270                                .or(self.handle.clone().map(AtIdentifier::Handle))
271                                .with_context(|| "Not logged in")?,
272                        }
273                        .into(),
274                    )
275                    .await?,
276            ),
277            Command::GetPreferences => self.print(
278                &self
279                    .agent
280                    .api
281                    .app
282                    .bsky
283                    .actor
284                    .get_preferences(
285                        atrium_api::app::bsky::actor::get_preferences::ParametersData {}.into(),
286                    )
287                    .await?,
288            ),
289            Command::ListNotifications => self.print(
290                &self
291                    .agent
292                    .api
293                    .app
294                    .bsky
295                    .notification
296                    .list_notifications(
297                        atrium_api::app::bsky::notification::list_notifications::ParametersData {
298                            cursor: None,
299                            limit: Some(limit),
300                            seen_at: None,
301                        }
302                        .into(),
303                    )
304                    .await?,
305            ),
306            Command::ListConvos => self.print(
307                &self
308                    .agent
309                    .api_with_proxy(
310                        BSKY_CHAT_DID.parse().expect("valid DID"),
311                        AtprotoServiceType::BskyChat,
312                    )
313                    .chat
314                    .bsky
315                    .convo
316                    .list_convos(
317                        atrium_api::chat::bsky::convo::list_convos::ParametersData {
318                            cursor: None,
319                            limit: Some(limit),
320                        }
321                        .into(),
322                    )
323                    .await?,
324            ),
325            Command::SendConvoMessage(args) => {
326                let did = match args.actor {
327                    AtIdentifier::Handle(handle) => self
328                        .agent
329                        .api
330                        .com
331                        .atproto
332                        .identity
333                        .resolve_handle(
334                            atrium_api::com::atproto::identity::resolve_handle::ParametersData {
335                                handle: handle.clone(),
336                            }
337                            .into(),
338                        )
339                        .await?
340                        .data
341                        .did,
342                    AtIdentifier::Did(did) => did,
343                };
344                let chat = &self
345                    .agent
346                    .api_with_proxy(
347                        BSKY_CHAT_DID.parse().expect("valid DID"),
348                        AtprotoServiceType::BskyChat,
349                    )
350                    .chat;
351                let convo = chat
352                    .bsky
353                    .convo
354                    .get_convo_for_members(
355                        atrium_api::chat::bsky::convo::get_convo_for_members::ParametersData {
356                            members: vec![did],
357                        }
358                        .into(),
359                    )
360                    .await?;
361                self.print(
362                    &chat
363                        .bsky
364                        .convo
365                        .send_message(
366                            atrium_api::chat::bsky::convo::send_message::InputData {
367                                convo_id: convo.data.convo.data.id,
368                                message: atrium_api::chat::bsky::convo::defs::MessageInputData {
369                                    embed: None,
370                                    facets: None,
371                                    text: args.text,
372                                }
373                                .into(),
374                            }
375                            .into(),
376                        )
377                        .await?,
378                )
379            }
380            Command::CreatePost(args) => {
381                let mut images = Vec::new();
382                for image in &args.images {
383                    if let Ok(mut file) = File::open(image).await {
384                        let mut buf = Vec::new();
385                        file.read_to_end(&mut buf).await.expect("read image file");
386                        let output = self
387                            .agent
388                            .api
389                            .com
390                            .atproto
391                            .repo
392                            .upload_blob(buf)
393                            .await
394                            .expect("upload blob");
395                        images.push(
396                            atrium_api::app::bsky::embed::images::ImageData {
397                                alt: image
398                                    .file_name()
399                                    .map(OsStr::to_string_lossy)
400                                    .unwrap_or_default()
401                                    .into(),
402                                aspect_ratio: None,
403                                image: output.data.blob,
404                            }
405                            .into(),
406                        )
407                    }
408                }
409                let embed = Some(atrium_api::types::Union::Refs(
410                    atrium_api::app::bsky::feed::post::RecordEmbedRefs::AppBskyEmbedImagesMain(
411                        Box::new(atrium_api::app::bsky::embed::images::MainData { images }.into()),
412                    ),
413                ));
414                self.print(
415                    &self
416                        .agent
417                        .api
418                        .com
419                        .atproto
420                        .repo
421                        .create_record(
422                            atrium_api::com::atproto::repo::create_record::InputData {
423                                collection: "app.bsky.feed.post".parse().expect("valid"),
424                                record: Record::Known(KnownRecord::AppBskyFeedPost(Box::new(
425                                    atrium_api::app::bsky::feed::post::RecordData {
426                                        created_at: Datetime::now(),
427                                        embed,
428                                        entities: None,
429                                        facets: None,
430                                        labels: None,
431                                        langs: None,
432                                        reply: None,
433                                        tags: None,
434                                        text: args.text,
435                                    }
436                                    .into(),
437                                ))),
438                                repo: self.handle.clone().with_context(|| "Not logged in")?.into(),
439                                rkey: None,
440                                swap_commit: None,
441                                validate: None,
442                            }
443                            .into(),
444                        )
445                        .await?,
446                )
447            }
448            Command::DeletePost(args) => self.print(
449                &self
450                    .agent
451                    .api
452                    .com
453                    .atproto
454                    .repo
455                    .delete_record(
456                        atrium_api::com::atproto::repo::delete_record::InputData {
457                            collection: "app.bsky.feed.post".parse().expect("valid"),
458                            repo: self.handle.clone().with_context(|| "Not logged in")?.into(),
459                            rkey: args.uri.rkey,
460                            swap_commit: None,
461                            swap_record: None,
462                        }
463                        .into(),
464                    )
465                    .await?,
466            ),
467        }
468    }
469    fn print<T: std::fmt::Debug + Serialize>(&self, result: &T) -> Result<()> {
470        if self.debug {
471            println!("{:#?}", result);
472        } else {
473            println!("{}", serde_json::to_string_pretty(result)?);
474        }
475        Ok(())
476    }
477}