feat: store AP note extensions in JSONB and render movies-diary posts as rich cards

This commit is contained in:
2026-05-24 04:29:04 +02:00
parent f4932af2ba
commit 5097c91261
17 changed files with 227 additions and 9 deletions

View File

@@ -14,6 +14,36 @@ use domain::ports::{EventPublisher, TagRepository};
use domain::value_objects::UserId;
use k_ap::ApObjectHandler;
fn extract_note_extensions(obj: &serde_json::Value) -> Option<serde_json::Value> {
const STANDARD: &[&str] = &[
"type",
"id",
"attributedTo",
"content",
"published",
"to",
"cc",
"inReplyTo",
"sensitive",
"summary",
"tag",
"url",
"@context",
"mediaType",
];
let extensions: serde_json::Map<String, serde_json::Value> = obj
.as_object()?
.iter()
.filter(|(k, _)| !STANDARD.contains(&k.as_str()))
.map(|(k, v)| (k.clone(), v.clone()))
.collect();
if extensions.is_empty() {
None
} else {
Some(serde_json::Value::Object(extensions))
}
}
pub struct ThoughtsObjectHandler {
repo: Arc<dyn ActivityPubRepository>,
urls: ThoughtsUrls,
@@ -118,6 +148,7 @@ impl ApObjectHandler for ThoughtsObjectHandler {
actor_url: &Url,
object: serde_json::Value,
) -> Result<()> {
let note_extensions = extract_note_extensions(&object);
let note: ThoughtNote = serde_json::from_value(object)?;
let author_id = self
.repo
@@ -153,6 +184,7 @@ impl ApObjectHandler for ThoughtsObjectHandler {
content_warning: note.summary,
visibility,
in_reply_to: note.in_reply_to.as_ref().map(|u| u.as_str()),
note_extensions,
})
.await
.map_err(|e| anyhow!("{e}"))?;
@@ -408,3 +440,46 @@ impl ApObjectHandler for ThoughtsObjectHandler {
.map_err(|e| anyhow!("{e}"))
}
}
#[cfg(test)]
mod extract_tests {
use super::extract_note_extensions;
#[test]
fn extracts_non_standard_fields() {
let obj = serde_json::json!({
"type": "Note",
"id": "https://example.com/notes/1",
"content": "hello",
"published": "2025-01-01T00:00:00Z",
"movieTitle": "Dune",
"rating": 5,
"posterUrl": "https://example.com/poster.jpg"
});
let ext = extract_note_extensions(&obj).unwrap();
assert_eq!(ext["movieTitle"], "Dune");
assert_eq!(ext["rating"], 5);
assert_eq!(ext["posterUrl"], "https://example.com/poster.jpg");
assert!(ext.get("type").is_none());
assert!(ext.get("content").is_none());
assert!(ext.get("id").is_none());
}
#[test]
fn returns_none_for_standard_only_note() {
let obj = serde_json::json!({
"type": "Note",
"content": "hello",
"published": "2025-01-01T00:00:00Z",
"to": ["https://www.w3.org/ns/activitystreams#Public"],
"tag": []
});
assert!(extract_note_extensions(&obj).is_none());
}
#[test]
fn returns_none_for_non_object() {
let obj = serde_json::json!("not an object");
assert!(extract_note_extensions(&obj).is_none());
}
}

View File

@@ -14,6 +14,7 @@ pub struct AcceptNoteInput<'a> {
pub content_warning: Option<String>,
pub visibility: &'a str,
pub in_reply_to: Option<&'a str>,
pub note_extensions: Option<serde_json::Value>,
}
/// AP-protocol endpoints for a locally-stored user (local or interned remote).