mod agent;
use crate::BskyAgent;
use crate::error::{Error, Result};
use atrium_api::agent::atp_agent::store::AtpSessionStore;
use atrium_api::com::atproto::repo::{
create_record, delete_record, get_record, list_records, put_record,
};
use atrium_api::types::{Collection, LimitedNonZeroU8, TryIntoUnknown, string::RecordKey};
use atrium_api::xrpc::XrpcClient;
use std::future::Future;
#[cfg_attr(not(target_arch = "wasm32"), trait_variant::make(Send))]
pub trait Record<T, S>
where
T: XrpcClient + Send + Sync,
S: AtpSessionStore + Send + Sync,
S::Error: std::error::Error + Send + Sync + 'static,
{
fn list(
agent: &BskyAgent<T, S>,
cursor: Option<String>,
limit: Option<LimitedNonZeroU8<100u8>>,
) -> impl Future<Output = Result<list_records::Output>>;
fn get(
agent: &BskyAgent<T, S>,
rkey: RecordKey,
) -> impl Future<Output = Result<get_record::Output>>;
fn put(
self,
agent: &BskyAgent<T, S>,
rkey: RecordKey,
) -> impl Future<Output = Result<put_record::Output>>;
fn create(self, agent: &BskyAgent<T, S>)
-> impl Future<Output = Result<create_record::Output>>;
fn delete(
agent: &BskyAgent<T, S>,
rkey: RecordKey,
) -> impl Future<Output = Result<delete_record::Output>>;
}
macro_rules! record_impl {
($collection:path, $record:path, $record_data:path) => {
impl<T, S> Record<T, S> for $record
where
T: XrpcClient + Send + Sync,
S: AtpSessionStore + Send + Sync,
S::Error: std::error::Error + Send + Sync + 'static,
{
async fn list(
agent: &BskyAgent<T, S>,
cursor: Option<String>,
limit: Option<LimitedNonZeroU8<100u8>>,
) -> Result<list_records::Output> {
let session = agent.get_session().await.ok_or(Error::NotLoggedIn)?;
Ok(agent
.api
.com
.atproto
.repo
.list_records(
atrium_api::com::atproto::repo::list_records::ParametersData {
collection: <$collection>::nsid(),
cursor,
limit,
repo: session.data.did.into(),
reverse: None,
}
.into(),
)
.await?)
}
async fn get(agent: &BskyAgent<T, S>, rkey: RecordKey) -> Result<get_record::Output> {
let session = agent.get_session().await.ok_or(Error::NotLoggedIn)?;
Ok(agent
.api
.com
.atproto
.repo
.get_record(
atrium_api::com::atproto::repo::get_record::ParametersData {
cid: None,
collection: <$collection>::nsid(),
repo: session.data.did.into(),
rkey,
}
.into(),
)
.await?)
}
async fn put(
self,
agent: &BskyAgent<T, S>,
rkey: RecordKey,
) -> Result<put_record::Output> {
let session = agent.get_session().await.ok_or(Error::NotLoggedIn)?;
Ok(agent
.api
.com
.atproto
.repo
.put_record(
atrium_api::com::atproto::repo::put_record::InputData {
collection: <$collection>::nsid(),
record: self.try_into_unknown()?,
repo: session.data.did.into(),
rkey,
swap_commit: None,
swap_record: None,
validate: None,
}
.into(),
)
.await?)
}
async fn create(self, agent: &BskyAgent<T, S>) -> Result<create_record::Output> {
let session = agent.get_session().await.ok_or(Error::NotLoggedIn)?;
Ok(agent
.api
.com
.atproto
.repo
.create_record(
atrium_api::com::atproto::repo::create_record::InputData {
collection: <$collection>::nsid(),
record: self.try_into_unknown()?,
repo: session.data.did.into(),
rkey: None,
swap_commit: None,
validate: None,
}
.into(),
)
.await?)
}
async fn delete(
agent: &BskyAgent<T, S>,
rkey: RecordKey,
) -> Result<delete_record::Output> {
let session = agent.get_session().await.ok_or(Error::NotLoggedIn)?;
Ok(agent
.api
.com
.atproto
.repo
.delete_record(
atrium_api::com::atproto::repo::delete_record::InputData {
collection: <$collection>::nsid(),
repo: session.data.did.into(),
rkey,
swap_commit: None,
swap_record: None,
}
.into(),
)
.await?)
}
}
impl<T, S> Record<T, S> for $record_data
where
T: XrpcClient + Send + Sync,
S: AtpSessionStore + Send + Sync,
S::Error: std::error::Error + Send + Sync + 'static,
{
async fn list(
agent: &BskyAgent<T, S>,
cursor: Option<String>,
limit: Option<LimitedNonZeroU8<100u8>>,
) -> Result<list_records::Output> {
<$record>::list(agent, cursor, limit).await
}
async fn get(agent: &BskyAgent<T, S>, rkey: RecordKey) -> Result<get_record::Output> {
<$record>::get(agent, rkey).await
}
async fn put(
self,
agent: &BskyAgent<T, S>,
rkey: RecordKey,
) -> Result<put_record::Output> {
<$record>::from(self).put(agent, rkey).await
}
async fn create(self, agent: &BskyAgent<T, S>) -> Result<create_record::Output> {
<$record>::from(self).create(agent).await
}
async fn delete(
agent: &BskyAgent<T, S>,
rkey: RecordKey,
) -> Result<delete_record::Output> {
<$record>::delete(agent, rkey).await
}
}
};
}
record_impl!(
atrium_api::com::atproto::lexicon::Schema,
atrium_api::com::atproto::lexicon::schema::Record,
atrium_api::com::atproto::lexicon::schema::RecordData
);
record_impl!(
atrium_api::app::bsky::actor::Profile,
atrium_api::app::bsky::actor::profile::Record,
atrium_api::app::bsky::actor::profile::RecordData
);
record_impl!(
atrium_api::app::bsky::feed::Generator,
atrium_api::app::bsky::feed::generator::Record,
atrium_api::app::bsky::feed::generator::RecordData
);
record_impl!(
atrium_api::app::bsky::feed::Like,
atrium_api::app::bsky::feed::like::Record,
atrium_api::app::bsky::feed::like::RecordData
);
record_impl!(
atrium_api::app::bsky::feed::Post,
atrium_api::app::bsky::feed::post::Record,
atrium_api::app::bsky::feed::post::RecordData
);
record_impl!(
atrium_api::app::bsky::feed::Postgate,
atrium_api::app::bsky::feed::postgate::Record,
atrium_api::app::bsky::feed::postgate::RecordData
);
record_impl!(
atrium_api::app::bsky::feed::Repost,
atrium_api::app::bsky::feed::repost::Record,
atrium_api::app::bsky::feed::repost::RecordData
);
record_impl!(
atrium_api::app::bsky::feed::Threadgate,
atrium_api::app::bsky::feed::threadgate::Record,
atrium_api::app::bsky::feed::threadgate::RecordData
);
record_impl!(
atrium_api::app::bsky::graph::Block,
atrium_api::app::bsky::graph::block::Record,
atrium_api::app::bsky::graph::block::RecordData
);
record_impl!(
atrium_api::app::bsky::graph::Follow,
atrium_api::app::bsky::graph::follow::Record,
atrium_api::app::bsky::graph::follow::RecordData
);
record_impl!(
atrium_api::app::bsky::graph::List,
atrium_api::app::bsky::graph::list::Record,
atrium_api::app::bsky::graph::list::RecordData
);
record_impl!(
atrium_api::app::bsky::graph::Listblock,
atrium_api::app::bsky::graph::listblock::Record,
atrium_api::app::bsky::graph::listblock::RecordData
);
record_impl!(
atrium_api::app::bsky::graph::Listitem,
atrium_api::app::bsky::graph::listitem::Record,
atrium_api::app::bsky::graph::listitem::RecordData
);
record_impl!(
atrium_api::app::bsky::graph::Starterpack,
atrium_api::app::bsky::graph::starterpack::Record,
atrium_api::app::bsky::graph::starterpack::RecordData
);
record_impl!(
atrium_api::app::bsky::graph::Verification,
atrium_api::app::bsky::graph::verification::Record,
atrium_api::app::bsky::graph::verification::RecordData
);
record_impl!(
atrium_api::app::bsky::labeler::Service,
atrium_api::app::bsky::labeler::service::Record,
atrium_api::app::bsky::labeler::service::RecordData
);
record_impl!(
atrium_api::chat::bsky::actor::Declaration,
atrium_api::chat::bsky::actor::declaration::Record,
atrium_api::chat::bsky::actor::declaration::RecordData
);
record_impl!(
atrium_api::app::bsky::actor::Status,
atrium_api::app::bsky::actor::status::Record,
atrium_api::app::bsky::actor::status::RecordData
);
record_impl!(
atrium_api::app::bsky::notification::Declaration,
atrium_api::app::bsky::notification::declaration::Record,
atrium_api::app::bsky::notification::declaration::RecordData
);
#[cfg(test)]
mod tests {
use super::*;
use crate::{agent::BskyAtpAgentBuilder, agent::tests::MockSessionStore, tests::FAKE_CID};
use atrium_api::types::string::Datetime;
use atrium_api::xrpc::http::{Request, Response};
use atrium_api::xrpc::types::Header;
use atrium_api::xrpc::{HttpClient, XrpcClient};
struct MockClient;
impl HttpClient for MockClient {
async fn send_http(
&self,
request: Request<Vec<u8>>,
) -> core::result::Result<
Response<Vec<u8>>,
Box<dyn std::error::Error + Send + Sync + 'static>,
> {
let body = match request.uri().path() {
"/xrpc/com.atproto.repo.createRecord" => {
serde_json::to_vec(&create_record::OutputData {
cid: FAKE_CID.parse().expect("invalid cid"),
commit: None,
uri: String::from("at://did:fake:handle.test/app.bsky.feed.post/somerkey"),
validation_status: None,
})?
}
"/xrpc/com.atproto.repo.deleteRecord" => {
serde_json::to_vec(&delete_record::OutputData { commit: None })?
}
_ => unreachable!(),
};
Ok(Response::builder()
.header(Header::ContentType, "application/json")
.status(200)
.body(body)?)
}
}
impl XrpcClient for MockClient {
fn base_uri(&self) -> String {
String::new()
}
}
#[tokio::test]
async fn actor_profile() -> Result<()> {
let agent = BskyAtpAgentBuilder::new(MockClient).store(MockSessionStore).build().await?;
let output = atrium_api::app::bsky::actor::profile::RecordData {
avatar: None,
banner: None,
created_at: None,
description: None,
display_name: None,
joined_via_starter_pack: None,
labels: None,
pinned_post: None,
pronouns: None,
website: None,
}
.create(&agent)
.await?;
assert_eq!(
output,
create_record::OutputData {
cid: FAKE_CID.parse().expect("invalid cid"),
commit: None,
uri: String::from("at://did:fake:handle.test/app.bsky.feed.post/somerkey"),
validation_status: None,
}
.into()
);
atrium_api::app::bsky::actor::profile::Record::delete(
&agent,
RecordKey::new("somerkey".into()).unwrap(),
)
.await?;
Ok(())
}
#[tokio::test]
async fn feed_post() -> Result<()> {
let agent = BskyAtpAgentBuilder::new(MockClient).store(MockSessionStore).build().await?;
let output = atrium_api::app::bsky::feed::post::RecordData {
created_at: Datetime::now(),
embed: None,
entities: None,
facets: None,
labels: None,
langs: None,
reply: None,
tags: None,
text: String::from("text"),
}
.create(&agent)
.await?;
assert_eq!(
output,
create_record::OutputData {
cid: FAKE_CID.parse().expect("invalid cid"),
commit: None,
uri: String::from("at://did:fake:handle.test/app.bsky.feed.post/somerkey"),
validation_status: None,
}
.into()
);
atrium_api::app::bsky::feed::post::Record::delete(
&agent,
RecordKey::new("somerkey".into()).unwrap(),
)
.await?;
Ok(())
}
#[tokio::test]
async fn graph_follow() -> Result<()> {
let agent = BskyAtpAgentBuilder::new(MockClient).store(MockSessionStore).build().await?;
let output = atrium_api::app::bsky::graph::follow::RecordData {
created_at: Datetime::now(),
subject: "did:fake:handle.test".parse().expect("invalid did"),
}
.create(&agent)
.await?;
assert_eq!(
output,
create_record::OutputData {
cid: FAKE_CID.parse().expect("invalid cid"),
commit: None,
uri: String::from("at://did:fake:handle.test/app.bsky.feed.post/somerkey"),
validation_status: None,
}
.into()
);
atrium_api::app::bsky::graph::follow::Record::delete(
&agent,
RecordKey::new("somerkey".into()).unwrap(),
)
.await?;
Ok(())
}
}