feat: better APIs; WIP timeline support

This commit is contained in:
nullishamy 2025-04-11 15:47:22 +01:00
parent 022e6f9c6d
commit ce3a9bfb26
Signed by: amy
SSH key fingerprint: SHA256:WmV0uk6WgAQvDJlM8Ld4mFPHZo02CLXXP5VkwQ5xtyk
19 changed files with 425 additions and 211 deletions

View file

@ -2,4 +2,5 @@ pub mod user;
pub mod apps;
pub mod instance;
pub mod status;
pub mod preferences;
pub mod preferences;
pub mod timeline;

View file

@ -1,14 +1,15 @@
use chrono::Local;
use main::ap::{self, http::HttpClient};
use rocket::{
FromForm,
FromForm, State,
form::Form,
post,
serde::{Deserialize, Serialize},
};
use rocket_db_pools::Connection;
use uuid::Uuid;
use main::ap;
use crate::{AuthenticatedUser, Db};
use crate::{AuthenticatedUser, Db, types::content};
#[derive(Serialize, Deserialize, Debug, FromForm)]
#[serde(crate = "rocket::serde")]
pub struct Status {
@ -16,25 +17,88 @@ pub struct Status {
}
#[post("/statuses", data = "<status>")]
pub async fn new_status(mut db: Connection<Db>, status: Form<Status>, user: AuthenticatedUser) {
pub async fn new_status(
mut db: Connection<Db>,
http: &State<HttpClient>,
status: Form<Status>,
user: AuthenticatedUser,
) {
let user = ap::User::from_actor_id(&user.actor_id, &mut **db).await;
let outbox = ap::Outbox::for_user(user.clone(), http);
let post_id = Uuid::new_v4();
let uri = format!("https://ferri.amy.mov/users/{}/posts/{}", user.username(), post_id);
let uri = format!(
"https://ferri.amy.mov/users/{}/posts/{}",
user.username(),
post_id
);
let id = user.id();
let now = Local::now().to_rfc3339();
let post = sqlx::query!(
r#"
INSERT INTO post (id, user_id, content)
VALUES (?1, ?2, ?3)
INSERT INTO post (id, user_id, content, created_at)
VALUES (?1, ?2, ?3, ?4)
RETURNING *
"#,
uri,
id,
status.status
status.status,
now
)
.fetch_one(&mut **db)
.await
.unwrap();
dbg!(user, status, post);
let actors = sqlx::query!("SELECT * FROM actor")
.fetch_all(&mut **db)
.await
.unwrap();
for record in actors {
// Don't send to ourselves
if &record.id == user.actor_id() {
continue
}
let create_id = format!("https://ferri.amy.mov/activities/{}", Uuid::new_v4());
let activity = ap::Activity {
id: create_id,
ty: ap::ActivityType::Create,
object: content::Post {
context: "https://www.w3.org/ns/activitystreams".to_string(),
id: uri.clone(),
content: status.status.clone(),
ty: "Note".to_string(),
ts: Local::now().to_rfc3339(),
to: vec![format!(
"https://ferri.amy.mov/users/{}/followers",
user.username()
)],
cc: vec!["https://www.w3.org/ns/activitystreams#Public".to_string()],
},
to: vec![format!(
"https://ferri.amy.mov/users/{}/followers",
user.username()
)],
cc: vec!["https://www.w3.org/ns/activitystreams#Public".to_string()],
..Default::default()
};
let actor = ap::Actor::from_raw(
record.id.clone(),
record.inbox.clone(),
record.outbox.clone(),
);
let req = ap::OutgoingActivity {
req: activity,
signed_by: format!("https://ferri.amy.mov/users/{}#main-key", user.username()),
to: actor,
};
req.save(&mut **db).await;
outbox.post(req).await;
}
}

View file

@ -0,0 +1,91 @@
use crate::{Db, endpoints::api::user::CredentialAcount};
use rocket::{
get,
serde::{Deserialize, Serialize, json::Json},
};
use rocket_db_pools::Connection;
pub type TimelineAccount = CredentialAcount;
#[derive(Debug, Serialize, Deserialize)]
#[serde(crate = "rocket::serde")]
pub struct TimelineStatus {
id: String,
created_at: String,
in_reply_to_id: Option<String>,
in_reply_to_account_id: Option<String>,
content: String,
visibility: String,
spoiler_text: String,
sensitive: bool,
uri: String,
url: String,
replies_count: i64,
reblogs_count: i64,
favourites_count: i64,
favourited: bool,
reblogged: bool,
muted: bool,
bookmarked: bool,
media_attachments: Vec<()>,
account: TimelineAccount,
}
#[get("/timelines/home?<limit>")]
pub async fn home(mut db: Connection<Db>, limit: i64) -> Json<Vec<TimelineStatus>> {
let posts = sqlx::query!(
r#"
SELECT p.id as "post_id", u.id as "user_id", p.content, u.username, u.display_name, u.actor_id FROM post p
INNER JOIN user u on p.user_id = u.id
"#
)
.fetch_all(&mut **db)
.await
.unwrap();
let mut out = Vec::<TimelineStatus>::new();
for record in posts {
out.push(TimelineStatus {
id: record.post_id.clone(),
created_at: "2025-04-10T22:12:09Z".to_string(),
in_reply_to_id: None,
in_reply_to_account_id: None,
content: record.content.clone(),
visibility: "public".to_string(),
spoiler_text: "".to_string(),
sensitive: false,
uri: record.post_id.clone(),
url: record.post_id.clone(),
replies_count: 0,
reblogs_count: 0,
favourites_count: 0,
favourited: false,
reblogged: false,
muted: false,
bookmarked: false,
media_attachments: vec![],
account: CredentialAcount {
id: record.actor_id.clone(),
username: record.username.clone(),
acct: record.username.clone(),
display_name: record.display_name.clone(),
locked: false,
bot: false,
created_at: "2025-04-10T22:12:09Z".to_string(),
attribution_domains: vec![],
note: "".to_string(),
url: record.actor_id.clone(),
avatar: "https://ferri.amy.mov/assets/pfp.png".to_string(),
avatar_static: "https://ferri.amy.mov/assets/pfp.png".to_string(),
header: "https://ferri.amy.mov/assets/pfp.png".to_string(),
header_static: "https://ferri.amy.mov/assets/pfp.png".to_string(),
followers_count: 1,
following_count: 1,
statuses_count: 1,
last_status_at: "2025-04-10T22:14:34Z".to_string(),
},
});
}
Json(out)
}

View file

@ -1,4 +1,3 @@
use chrono::Local;
use main::ap;
use rocket::{
State, get, post,
@ -72,7 +71,7 @@ pub async fn new_follow(
id: format!("https://ferri.amy.mov/activities/{}", Uuid::new_v4()),
ty: ap::ActivityType::Follow,
object: followed.actor_id().to_string(),
published: Local::now(),
..Default::default()
};
let req = ap::OutgoingActivity {
@ -84,5 +83,6 @@ pub async fn new_follow(
to: followed.actor().clone(),
};
req.save(&mut **db).await;
outbox.post(req).await;
}