feat: basic Announce support

This commit is contained in:
nullishamy 2025-04-26 12:17:32 +01:00
parent 3719fae102
commit 2270324711
Signed by: amy
SSH key fingerprint: SHA256:WmV0uk6WgAQvDJlM8Ld4mFPHZo02CLXXP5VkwQ5xtyk
10 changed files with 178 additions and 4 deletions

View file

@ -111,6 +111,7 @@ async fn create_status(
favourites_count: 0,
favourited: false,
reblogged: false,
reblog: None,
muted: false,
bookmarked: false,
media_attachments: vec![],

View file

@ -27,6 +27,7 @@ pub struct TimelineStatus {
pub reblogged: bool,
pub muted: bool,
pub bookmarked: bool,
pub reblog: Option<Box<TimelineStatus>>,
pub media_attachments: Vec<()>,
pub account: TimelineAccount,
}
@ -40,7 +41,7 @@ pub async fn home(
let posts = sqlx::query!(
r#"
SELECT p.id as "post_id", u.id as "user_id", p.content, p.uri as "post_uri",
u.username, u.display_name, u.actor_id, p.created_at
u.username, u.display_name, u.actor_id, p.created_at, p.boosted_post_id
FROM post p
INNER JOIN user u on p.user_id = u.id
"#
@ -51,6 +52,64 @@ pub async fn home(
let mut out = Vec::<TimelineStatus>::new();
for record in posts {
let mut boost: Option<Box<TimelineStatus>> = None;
if let Some(boosted_id) = record.boosted_post_id {
let record = sqlx::query!(
r#"
SELECT p.id as "post_id", u.id as "user_id", p.content, p.uri as "post_uri",
u.username, u.display_name, u.actor_id, p.created_at, p.boosted_post_id
FROM post p
INNER JOIN user u on p.user_id = u.id
WHERE p.id = ?1
"#, boosted_id)
.fetch_one(&mut **db)
.await
.unwrap();
let user_uri = format!("https://ferri.amy.mov/users/{}", record.user_id);
boost = Some(Box::new(TimelineStatus {
id: record.post_id.clone(),
created_at: record.created_at.clone(),
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_uri.clone(),
url: record.post_uri.clone(),
replies_count: 0,
reblogs_count: 0,
favourites_count: 0,
favourited: false,
reblogged: false,
reblog: boost,
muted: false,
bookmarked: false,
media_attachments: vec![],
account: CredentialAcount {
id: record.user_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: user_uri,
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(),
},
}))
}
let user_uri = format!("https://ferri.amy.mov/users/{}", record.username);
out.push(TimelineStatus {
id: record.post_id.clone(),
@ -68,6 +127,7 @@ pub async fn home(
favourites_count: 0,
favourited: false,
reblogged: false,
reblog: boost,
muted: false,
bookmarked: false,
media_attachments: vec![],

View file

@ -165,6 +165,7 @@ pub async fn statuses(
favourites_count: 0,
favourited: false,
reblogged: false,
reblog: None,
muted: false,
bookmarked: false,
media_attachments: vec![],

View file

@ -99,6 +99,7 @@ pub async fn test(http: &State<HttpClient>) -> &'static str {
ts: "2025-04-10T10:48:11Z".to_string(),
to: vec!["https://ferri.amy.mov/users/amy/followers".to_string()],
cc: vec!["https://www.w3.org/ns/activitystreams#Public".to_string()],
attributed_to: None
},
ts: "2025-04-10T10:48:11Z".to_string(),
to: vec!["https://ferri.amy.mov/users/amy/followers".to_string()],

View file

@ -11,7 +11,7 @@ use tracing::{event, span, Level, debug, warn, info};
use crate::{
Db,
http::HttpClient,
types::{Person, activity},
types::{Person, content::Post, activity},
};
fn handle_delete_activity(activity: activity::DeleteActivity) {
@ -142,6 +142,92 @@ async fn handle_like_activity(activity: activity::LikeActivity, mut db: Connecti
}
}
async fn handle_boost_activity(
activity: activity::BoostActivity,
http: &HttpClient,
mut db: Connection<Db>,
) {
let key_id = "https://ferri.amy.mov/users/amy#main-key";
dbg!(&activity);
let post = http
.get(&activity.object)
.activity()
.sign(&key_id)
.send()
.await
.unwrap()
.json::<Post>()
.await
.unwrap();
dbg!(&post);
let attribution = post.attributed_to.unwrap();
let post_user = http
.get(&attribution)
.activity()
.sign(&key_id)
.send()
.await
.unwrap()
.json::<Person>()
.await
.unwrap();
let user = http
.get(&activity.actor)
.activity()
.sign(&key_id)
.send()
.await
.unwrap()
.json::<Person>()
.await
.unwrap();
dbg!(&post_user);
debug!("creating actor {}", activity.actor);
create_actor(&user, &activity.actor, &mut **db).await;
debug!("creating user {}", activity.actor);
create_user(&user, &activity.actor, &mut **db).await;
debug!("creating actor {}", attribution);
create_actor(&post_user, &attribution, &mut **db).await;
debug!("creating user {}", attribution);
create_user(&post_user, &attribution, &mut **db).await;
let attributed_user = ap::User::from_actor_id(&attribution, &mut **db).await;
let actor_user = ap::User::from_actor_id(&activity.actor, &mut **db).await;
let base_id = ap::new_id();
let now = ap::new_ts();
let reblog_id = ap::new_id();
let attr_id = attributed_user.id();
sqlx::query!("
INSERT INTO post (id, uri, user_id, content, created_at)
VALUES (?1, ?2, ?3, ?4, ?5)
", reblog_id, post.id, attr_id, post.content, post.ts)
.execute(&mut **db)
.await
.unwrap();
let uri = format!("https://ferri.amy.mov/users/{}/posts/{}", actor_user.id(), post.id);
let user_id = actor_user.id();
sqlx::query!("
INSERT INTO post (id, uri, user_id, content, created_at, boosted_post_id)
VALUES (?1, ?2, ?3, ?4, ?5, ?6)
", base_id, uri, user_id, "", now, reblog_id)
.execute(&mut **db)
.await
.unwrap();
}
async fn handle_create_activity(
activity: activity::CreateActivity,
http: &HttpClient,
@ -218,6 +304,11 @@ pub async fn inbox(db: Connection<Db>, http: &State<HttpClient>, user: &str, bod
let activity = serde_json::from_str::<activity::LikeActivity>(&body).unwrap();
handle_like_activity(activity, db).await;
}
"Announce" => {
let activity = serde_json::from_str::<activity::BoostActivity>(&body).unwrap();
handle_boost_activity(activity, http.inner(), db).await;
}
act => {
warn!(act, body, "unknown activity");
}

View file

@ -107,6 +107,7 @@ pub async fn post(
Json(content::Post {
context: "https://www.w3.org/ns/activitystreams".to_string(),
id: format!("https://ferri.amy.mov/users/{}/posts/{}", uuid, post.id),
attributed_to: Some(format!("https://ferri.amy.mov/users/{}/posts/{}", uuid, post.id)),
ty: "Note".to_string(),
content: post.content,
ts: post.created_at,

View file

@ -1,5 +1,4 @@
use rocket::serde::{Deserialize, Serialize};
use crate::types::content::Post;
#[derive(Serialize, Deserialize, Debug)]
@ -60,3 +59,17 @@ pub struct AcceptActivity {
pub object: String,
pub actor: String,
}
#[derive(Serialize, Deserialize, Debug)]
#[serde(crate = "rocket::serde")]
pub struct BoostActivity {
#[serde(rename = "type")]
pub ty: String,
pub id: String,
pub actor: String,
pub published: String,
pub to: Vec<String>,
pub cc: Vec<String>,
pub object: String,
}

View file

@ -15,4 +15,7 @@ pub struct Post {
pub content: String,
pub to: Vec<String>,
pub cc: Vec<String>,
#[serde(rename = "attributedTo")]
pub attributed_to: Option<String>
}

View file

@ -34,6 +34,7 @@ pub struct Person {
pub inbox: String,
pub outbox: String,
pub preferred_username: String,
#[serde(default)]
pub name: String,
pub public_key: Option<UserKey>,
}

View file

@ -5,6 +5,8 @@ CREATE TABLE IF NOT EXISTS post
user_id TEXT NOT NULL,
content TEXT NOT NULL,
created_at TEXT NOT NULL,
boosted_post_id TEXT,
FOREIGN KEY(user_id) REFERENCES user(id)
FOREIGN KEY(user_id) REFERENCES user(id),
FOREIGN KEY(boosted_post_id) REFERENCES post(id)
);