Round 1 — 18 bug fixes:
- remote likes/boosts now persist in engagement tables
- intern_remote_actor uses name@domain, expanded username to VARCHAR(255)
- PgRemoteActorRepository upsert/find now handles all fields
- update_following_status no longer a no-op, count_followers counts all
- accept/reject follow publishes event before DB mark (atomicity)
- fetch_outbox_page follows pagination via next links
- actor URL canonicalized to /users/{uuid}
- content_to_html escapes single quotes
- WebFinger accepts application/ld+json type
- try_from_ap accepts Article and Page object types
- feed SQL uses parameterized viewer UUID instead of format!
- content cap raised from 500 to 5000 chars
- also_known_as changed from Option<String> to Vec<String>
- connections fetch always triggers from page 1
Round 2 — 9 gap fixes:
- on_announce_removed handler deletes boost row on Undo(Announce)
- on_update handles Person/Service/Group actor profile updates
- sync_remote_actor_to_user syncs remote_actors → users on create/update
- FederationBlockPort: block_by_username sends Block activity to remote
- domain RemoteActor gains inbox_url, shared_inbox_url fields
- remote_actors attachment column (JSONB) with read/write
- .well-known/host-meta endpoint
- 256KB body limit on AP inbox routes
- outbox cleanup job (7-day retention, hourly sweep)
111 lines
3.1 KiB
Rust
111 lines
3.1 KiB
Rust
use chrono::{DateTime, Utc};
|
|
use k_ap::NoteType;
|
|
use k_ap::AS_PUBLIC;
|
|
use serde::{Deserialize, Serialize};
|
|
use url::Url;
|
|
|
|
const STANDARD_NOTE_FIELDS: &[&str] = &[
|
|
"type",
|
|
"id",
|
|
"attributedTo",
|
|
"content",
|
|
"published",
|
|
"to",
|
|
"cc",
|
|
"inReplyTo",
|
|
"sensitive",
|
|
"summary",
|
|
"tag",
|
|
"url",
|
|
"@context",
|
|
"mediaType",
|
|
];
|
|
|
|
pub fn extract_extensions(obj: &serde_json::Value) -> Option<serde_json::Value> {
|
|
let extensions: serde_json::Map<String, serde_json::Value> = obj
|
|
.as_object()?
|
|
.iter()
|
|
.filter(|(k, _)| !STANDARD_NOTE_FIELDS.contains(&k.as_str()))
|
|
.map(|(k, v)| (k.clone(), v.clone()))
|
|
.collect();
|
|
if extensions.is_empty() {
|
|
None
|
|
} else {
|
|
Some(serde_json::Value::Object(extensions))
|
|
}
|
|
}
|
|
|
|
/// AP Note representing a Thought.
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct ThoughtNote {
|
|
#[serde(rename = "type")]
|
|
pub kind: NoteType,
|
|
pub id: Url,
|
|
#[serde(skip_serializing_if = "Option::is_none", default)]
|
|
pub url: Option<Url>,
|
|
pub attributed_to: Url,
|
|
pub content: String,
|
|
pub published: DateTime<Utc>,
|
|
#[serde(skip_serializing_if = "Vec::is_empty", default)]
|
|
pub to: Vec<String>,
|
|
#[serde(skip_serializing_if = "Vec::is_empty", default)]
|
|
pub cc: Vec<String>,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub in_reply_to: Option<Url>,
|
|
#[serde(default)]
|
|
pub sensitive: bool,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub summary: Option<String>,
|
|
#[serde(skip_serializing_if = "Vec::is_empty", default)]
|
|
pub tag: Vec<serde_json::Value>,
|
|
}
|
|
|
|
pub struct ThoughtNoteInput {
|
|
pub id: Url,
|
|
pub actor_url: Url,
|
|
pub content: String,
|
|
pub published: DateTime<Utc>,
|
|
pub in_reply_to: Option<Url>,
|
|
pub sensitive: bool,
|
|
pub summary: Option<String>,
|
|
pub followers_url: Url,
|
|
}
|
|
|
|
impl ThoughtNote {
|
|
/// Returns `(note, extensions)` if `value` is a Note object, `None` otherwise.
|
|
pub fn try_from_ap(mut value: serde_json::Value) -> Option<(Self, Option<serde_json::Value>)> {
|
|
let obj_type = value.get("type").and_then(|v| v.as_str());
|
|
if !matches!(obj_type, Some("Note" | "Article" | "Page")) {
|
|
return None;
|
|
}
|
|
let extensions = extract_extensions(&value);
|
|
if let Some(obj) = value.as_object_mut() {
|
|
obj.insert("type".to_string(), serde_json::json!("Note"));
|
|
}
|
|
serde_json::from_value(value)
|
|
.ok()
|
|
.map(|note| (note, extensions))
|
|
}
|
|
|
|
pub fn new_public(p: ThoughtNoteInput) -> Self {
|
|
Self {
|
|
kind: Default::default(),
|
|
url: Some(p.id.clone()),
|
|
id: p.id,
|
|
attributed_to: p.actor_url,
|
|
content: p.content,
|
|
published: p.published,
|
|
to: vec![AS_PUBLIC.to_string()],
|
|
cc: vec![p.followers_url.to_string()],
|
|
in_reply_to: p.in_reply_to,
|
|
sensitive: p.sensitive,
|
|
summary: p.summary,
|
|
tag: Vec::new(),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests;
|