implement basic content posting without images

This commit is contained in:
Denis-Cosmin NUTIU 2024-12-30 17:48:07 +02:00
parent 8512c0e253
commit f8827d3ba4
5 changed files with 175 additions and 8 deletions

View file

@ -16,3 +16,4 @@ serde = { version = "1.0.216", features = ["derive"] }
base64 = "0.22.1"
serde_json = "1.0.134"
reqwest = { version = "0.12.11", features = ["json"] }
chrono = "0.4.39"

View file

@ -1,8 +1,9 @@
mod atproto;
pub(crate) mod atproto;
mod token;
use crate::bluesky::atproto::ATProtoServerCreateSession;
use crate::token::Token;
use reqwest::Body;
use token::Token;
/// The BlueSky client used to interact with the platform.
pub struct BlueSkyClient {

View file

@ -1,7 +1,164 @@
use chrono::{DateTime, Local, Utc};
use post::NewsPost;
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Debug, PartialEq, Default)]
#[derive(Serialize, Deserialize, Debug, Default)]
pub struct ATProtoServerCreateSession {
pub(crate) identifier: String,
pub(crate) password: String,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct ExternalRecordEmbed {
uri: String,
title: String,
description: String,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct ATprotoRepoCreateRecordEmbed {
#[serde(rename(serialize = "$type", deserialize = "$type"))]
embed_type: String,
external: ExternalRecordEmbed,
}
impl ATprotoRepoCreateRecordEmbed {
fn new(uri: &str, title: &str, description: &str) -> Self {
ATprotoRepoCreateRecordEmbed {
embed_type: "app.bsky.embed.external".to_string(),
external: ExternalRecordEmbed {
uri: uri.to_string(),
title: title.to_string(),
description: description.to_string(),
},
}
}
}
#[derive(Serialize, Deserialize, Debug)]
pub struct ATprotoRepoCreateRecordRecord {
text: String,
#[serde(rename(serialize = "createdAt", deserialize = "createdAt"))]
created_at: String,
#[serde(skip_serializing_if = "Option::is_none")]
embed: Option<ATprotoRepoCreateRecordEmbed>,
}
impl ATprotoRepoCreateRecordRecord {
fn new(text: &str, date: DateTime<Utc>, embed: Option<ATprotoRepoCreateRecordEmbed>) -> Self {
ATprotoRepoCreateRecordRecord {
text: text.to_string(),
created_at: date.to_rfc3339(),
embed,
}
}
}
#[derive(Serialize, Deserialize, Debug)]
pub struct ATProtoRepoCreateRecord {
pub repo: String,
collection: String,
record: ATprotoRepoCreateRecordRecord,
}
impl ATProtoRepoCreateRecord {
fn new(handle: &str, record: ATprotoRepoCreateRecordRecord) -> Self {
ATProtoRepoCreateRecord {
repo: handle.to_string(),
collection: "app.bsky.feed.post".to_string(),
record,
}
}
}
impl From<NewsPost> for ATProtoRepoCreateRecord {
fn from(post: NewsPost) -> Self {
let dt = Local::now();
let dt_utc = DateTime::<Utc>::from_naive_utc_and_offset(dt.naive_utc(), Utc);
ATProtoRepoCreateRecord::new(
"",
ATprotoRepoCreateRecordRecord::new(
post.title.clone().unwrap_or(String::from("")).as_str(),
dt_utc,
Some(ATprotoRepoCreateRecordEmbed::new(
post.link.unwrap().as_str(),
post.title.unwrap().as_str(),
post.summary.unwrap().as_str(),
)),
),
)
}
}
#[cfg(test)]
mod tests {
use super::*;
use chrono::NaiveDateTime;
#[test]
fn test_atproto_server_create_session_serialization() -> Result<(), anyhow::Error> {
let session = ATProtoServerCreateSession {
identifier: "user".to_string(),
password: "pass".to_string(),
};
let json = serde_json::to_string(&session)?;
assert_eq!(json, r#"{"identifier":"user","password":"pass"}"#);
Ok(())
}
#[test]
fn test_atproto_repo_create_record_record_no_embed_serialization() -> Result<(), anyhow::Error>
{
let time_str = "2024-12-30T13:45:00";
let naive_datetime = NaiveDateTime::parse_from_str(time_str, "%Y-%m-%dT%H:%M:%S").unwrap();
let session = ATProtoRepoCreateRecord::new(
"nuculabs.dev",
ATprotoRepoCreateRecordRecord::new(
"some post",
DateTime::from_naive_utc_and_offset(naive_datetime, Utc),
None,
),
);
let json = serde_json::to_string(&session)?;
assert_eq!(
json,
r#"{"repo":"nuculabs.dev","collection":"app.bsky.feed.post","record":{"text":"some post","createdAt":"2024-12-30T13:45:00+00:00"}}"#
);
Ok(())
}
#[test]
fn test_atproto_repo_create_record_record_embed_serialization() -> Result<(), anyhow::Error> {
let time_str = "2024-12-30T13:45:00";
let naive_datetime = NaiveDateTime::parse_from_str(time_str, "%Y-%m-%dT%H:%M:%S").unwrap();
let session = ATProtoRepoCreateRecord::new(
"nuculabs.dev",
ATprotoRepoCreateRecordRecord::new(
"some post",
DateTime::from_naive_utc_and_offset(naive_datetime, Utc),
Some(ATprotoRepoCreateRecordEmbed::new(
"https://some-news.ro/some",
"Some very important news",
"The description of the news",
)),
),
);
let json = serde_json::to_string(&session)?;
assert_eq!(
json,
r#"{"repo":"nuculabs.dev","collection":"app.bsky.feed.post","record":{"text":"some post","createdAt":"2024-12-30T13:45:00+00:00","embed":{"$type":"app.bsky.embed.external","external":{"uri":"https://some-news.ro/some","title":"Some very important news","description":"The description of the news"}}}}"#
);
Ok(())
}
}

View file

@ -12,7 +12,6 @@ use std::thread;
mod bluesky;
mod cli;
mod token;
//noinspection DuplicatedCode
/// Sets up a signal handler in a separate thread to handle SIGINT and SIGTERM signals.
@ -69,11 +68,20 @@ async fn main() -> Result<(), anyhow::Error> {
.await
{
Ok(post) => {
let data = ""; // TODO
if let Err(err) = bluesky_client.post(data).await {
let mut data: bluesky::atproto::ATProtoRepoCreateRecord = post.clone().into();
data.repo = args.bluesky_handle.clone();
let json = serde_json::to_string(&data);
match json {
Ok(json) => {
if let Err(err) = bluesky_client.post(json).await {
error!("failed to post: {post:?} {err}")
}
}
Err(err) => {
error!("failed to convert post to json: {post:?} {err}")
}
}
}
Err(err) => {
error!("error reading stream: {err}")
}