refactor: 5 architectural improvements (Tasks 2-5 + Task 6 fix)
Some checks failed
lint / lint (push) Has been cancelled
test / unit (push) Has been cancelled
test / integration (push) Has been cancelled
lint / lint (pull_request) Failing after 5m2s
test / unit (pull_request) Successful in 16m19s
test / integration (pull_request) Failing after 17m15s
Some checks failed
lint / lint (push) Has been cancelled
test / unit (push) Has been cancelled
test / integration (push) Has been cancelled
lint / lint (pull_request) Failing after 5m2s
test / unit (pull_request) Successful in 16m19s
test / integration (pull_request) Failing after 17m15s
- feat(domain): Hashtag value object with canonical extract() — unifies two divergent private implementations; fields pre-compute raw/normalized/url_slug/ap_name - feat(presentation): Deps<S: FromAppState> extractor — each handler now declares its exact dependency surface; AppState unchanged; handlers become unit-testable without mocking all 20 deps - refactor(feed): replace 5 flat FeedRepository methods with FeedQuery/FeedScope — single query() method; SQL shared logic lives once; adding feed types no longer requires 5 edits - refactor(activitypub): ActivityPubRepository + OutboundFederationPort moved out of domain::ports into activitypub-base::ap_ports — domain crate no longer knows about AP IDs, inboxes, or actor URLs - fix(outbox): OutboxRelay now opens a per-row transaction so FOR UPDATE SKIP LOCKED actually holds the lock during publish + mark_delivered
This commit is contained in:
@@ -50,26 +50,6 @@ fn content_to_html(text: &str) -> String {
|
||||
}
|
||||
}
|
||||
|
||||
fn extract_hashtag_tags(content: &str, base_url: &str) -> Vec<serde_json::Value> {
|
||||
let mut seen = std::collections::HashSet::new();
|
||||
let mut tags = Vec::new();
|
||||
for word in content.split_whitespace() {
|
||||
let tag = word.trim_matches(|c: char| !c.is_alphanumeric() && c != '#');
|
||||
if let Some(name) = tag.strip_prefix('#')
|
||||
&& !name.is_empty()
|
||||
&& seen.insert(name.to_lowercase())
|
||||
{
|
||||
let lower = name.to_lowercase();
|
||||
tags.push(serde_json::json!({
|
||||
"type": "Hashtag",
|
||||
"name": format!("#{}", lower),
|
||||
"href": format!("{}/tags/{}", base_url, lower),
|
||||
}));
|
||||
}
|
||||
}
|
||||
tags
|
||||
}
|
||||
|
||||
fn thought_note_json(
|
||||
thought: &domain::models::thought::Thought,
|
||||
local_actor: &crate::actors::DbActor,
|
||||
@@ -114,9 +94,19 @@ fn thought_note_json(
|
||||
if let Some(updated_at) = thought.updated_at {
|
||||
note["updated"] = serde_json::json!(updated_at.to_rfc3339());
|
||||
}
|
||||
let hashtag_tags = extract_hashtag_tags(thought.content.as_str(), base_url);
|
||||
if !hashtag_tags.is_empty() {
|
||||
note["tag"] = serde_json::json!(hashtag_tags);
|
||||
let hashtags = domain::hashtag::extract(thought.content.as_str());
|
||||
if !hashtags.is_empty() {
|
||||
let ap_tags: Vec<serde_json::Value> = hashtags
|
||||
.iter()
|
||||
.map(|h| {
|
||||
serde_json::json!({
|
||||
"type": "Hashtag",
|
||||
"name": h.ap_name,
|
||||
"href": format!("{}/{}", base_url, h.url_slug),
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
note["tag"] = serde_json::json!(ap_tags);
|
||||
}
|
||||
Ok((ap_id, note))
|
||||
}
|
||||
@@ -1405,7 +1395,7 @@ impl ActivityPubService {
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl domain::ports::OutboundFederationPort for ActivityPubService {
|
||||
impl crate::ap_ports::OutboundFederationPort for ActivityPubService {
|
||||
// Actor identity (ap_id, followers_url) comes from federation config via get_local_actor.
|
||||
// author_username is provided by the caller but not needed here.
|
||||
async fn broadcast_create(
|
||||
|
||||
Reference in New Issue
Block a user