fix: pre-release improvements — scale, correctness, API clarity
#1 count_accepted_followers / get_accepted_followers_page: new DB-side methods on FollowRepository — no more loading all followers into memory to count or page them. #2 Move re-follows are now non-blocking: tokio::spawn instead of awaiting each sign_and_send inside receive() — inbox handler no longer stalls for slow remote servers during account migration. #3 Remove get_local_objects_for_user from ApContentReader (dead code). Backfill and outbox both use the paginated get_local_objects_page. #6 Rename backfill_outbox → import_remote_outbox with a clear doc explaining the direction (import FROM a remote server, not to a follower). #7 also_known_as: Option<String> → Vec<String> on ApUser, LookedUpActor, and DbActor. from_json now stores all aliases; move_act.rs checks all. Mentions: broadcast_create_note / broadcast_update_note now accept mentioned_inboxes: Vec<Url> — delivery goes to followers + mentioned actors who aren't already followers. Deduplication is done before sending. Pass vec![] if note has no external mentions. Docs: ApObjectHandler and ApContentReader now have complete doc comments with contracts, idempotency guidance, and error-handling semantics.
This commit is contained in:
@@ -56,26 +56,42 @@ impl Activity for MoveActivity {
|
|||||||
.dereference(data)
|
.dereference(data)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| Error::from(anyhow::anyhow!("{e}")))?;
|
.map_err(|e| Error::from(anyhow::anyhow!("{e}")))?;
|
||||||
if target.also_known_as.as_deref() != Some(self.object.as_str()) {
|
// Verify the new actor claims the old identity via alsoKnownAs.
|
||||||
|
// The spec allows multiple aliases; check all of them.
|
||||||
|
let old_url = self.object.as_str();
|
||||||
|
if !target.also_known_as.iter().any(|a| a == old_url) {
|
||||||
return Err(Error::bad_request(anyhow::anyhow!(
|
return Err(Error::bad_request(anyhow::anyhow!(
|
||||||
"Move target alsoKnownAs does not reference old actor"
|
"Move target alsoKnownAs does not reference old actor"
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
let affected = data
|
let affected = data
|
||||||
.follow_repo
|
.follow_repo
|
||||||
.migrate_follower_actor(self.object.as_str(), self.target.as_str())
|
.migrate_follower_actor(old_url, self.target.as_str())
|
||||||
.await
|
.await
|
||||||
.map_err(|e| Error::from(anyhow::anyhow!("{e}")))?;
|
.map_err(|e| Error::from(anyhow::anyhow!("{e}")))?;
|
||||||
let affected_count = affected.len();
|
let affected_count = affected.len();
|
||||||
|
|
||||||
|
// Spawn re-follows in the background — do NOT await them inside receive()
|
||||||
|
// to avoid blocking the inbox handler while making outbound HTTP requests.
|
||||||
|
let target_inbox = target.inbox_url.clone();
|
||||||
|
let target_url = self.target.clone();
|
||||||
|
let base_url = data.base_url.clone();
|
||||||
|
let data_clone = data.clone();
|
||||||
|
tokio::spawn(async move {
|
||||||
for local_user_id in &affected {
|
for local_user_id in &affected {
|
||||||
let local_actor = match crate::actors::get_local_actor(*local_user_id, data).await {
|
let local_actor =
|
||||||
|
match crate::actors::get_local_actor(*local_user_id, &data_clone).await {
|
||||||
Ok(a) => a,
|
Ok(a) => a,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
tracing::warn!(error = %e, %local_user_id, "Move: failed to load local actor");
|
tracing::warn!(
|
||||||
|
error = %e,
|
||||||
|
%local_user_id,
|
||||||
|
"Move: failed to load local actor"
|
||||||
|
);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let follow_id = match crate::urls::activity_url(&data.base_url) {
|
let follow_id = match crate::urls::activity_url(&base_url) {
|
||||||
Ok(u) => u,
|
Ok(u) => u,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
tracing::warn!(error = %e, "Move: failed to generate follow activity URL");
|
tracing::warn!(error = %e, "Move: failed to generate follow activity URL");
|
||||||
@@ -86,13 +102,13 @@ impl Activity for MoveActivity {
|
|||||||
id: follow_id,
|
id: follow_id,
|
||||||
kind: Default::default(),
|
kind: Default::default(),
|
||||||
actor: ObjectId::from(local_actor.ap_id.clone()),
|
actor: ObjectId::from(local_actor.ap_id.clone()),
|
||||||
object: ObjectId::from(self.target.clone()),
|
object: ObjectId::from(target_url.clone()),
|
||||||
};
|
};
|
||||||
let sends = match SendActivityTask::prepare(
|
let sends = match SendActivityTask::prepare(
|
||||||
&WithContext::new_default(follow),
|
&WithContext::new_default(follow),
|
||||||
&local_actor,
|
&local_actor,
|
||||||
vec![target.inbox_url.clone()],
|
vec![target_inbox.clone()],
|
||||||
data,
|
&data_clone,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
@@ -103,16 +119,22 @@ impl Activity for MoveActivity {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
for send in sends {
|
for send in sends {
|
||||||
if let Err(e) = send.sign_and_send(data).await {
|
if let Err(e) = send.sign_and_send(&data_clone).await {
|
||||||
tracing::warn!(error = %e, %local_user_id, "Move: re-follow delivery failed");
|
tracing::warn!(
|
||||||
|
error = %e,
|
||||||
|
%local_user_id,
|
||||||
|
"Move: re-follow delivery failed"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
tracing::info!(
|
tracing::info!(
|
||||||
actor = %self.actor.inner(),
|
actor = %self.actor.inner(),
|
||||||
target = %self.target,
|
target = %self.target,
|
||||||
affected = affected_count,
|
affected = affected_count,
|
||||||
"received Move — migrated follower relationships"
|
"received Move — migrated follower relationships, re-follows spawned"
|
||||||
);
|
);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ pub struct DbActor {
|
|||||||
pub bio: Option<String>,
|
pub bio: Option<String>,
|
||||||
pub avatar_url: Option<Url>,
|
pub avatar_url: Option<Url>,
|
||||||
pub banner_url: Option<Url>,
|
pub banner_url: Option<Url>,
|
||||||
pub also_known_as: Option<String>,
|
pub also_known_as: Vec<String>,
|
||||||
pub profile_url: Option<Url>,
|
pub profile_url: Option<Url>,
|
||||||
pub attachment: Vec<ApProfileField>,
|
pub attachment: Vec<ApProfileField>,
|
||||||
pub manually_approves_followers: bool,
|
pub manually_approves_followers: bool,
|
||||||
@@ -283,7 +283,7 @@ impl Object for DbActor {
|
|||||||
kind: "Image".to_string(),
|
kind: "Image".to_string(),
|
||||||
url,
|
url,
|
||||||
});
|
});
|
||||||
let also_known_as: Vec<String> = self.also_known_as.into_iter().collect();
|
let also_known_as = self.also_known_as;
|
||||||
let attachment: Vec<ProfileFieldObject> = self
|
let attachment: Vec<ProfileFieldObject> = self
|
||||||
.attachment
|
.attachment
|
||||||
.into_iter()
|
.into_iter()
|
||||||
@@ -395,7 +395,7 @@ impl Object for DbActor {
|
|||||||
bio: json.summary.clone(),
|
bio: json.summary.clone(),
|
||||||
avatar_url: json.icon.as_ref().map(|i| i.url.clone()),
|
avatar_url: json.icon.as_ref().map(|i| i.url.clone()),
|
||||||
banner_url: json.image.as_ref().map(|i| i.url.clone()),
|
banner_url: json.image.as_ref().map(|i| i.url.clone()),
|
||||||
also_known_as: json.also_known_as.into_iter().next(),
|
also_known_as: json.also_known_as,
|
||||||
profile_url: json.url.clone(),
|
profile_url: json.url.clone(),
|
||||||
attachment: json
|
attachment: json
|
||||||
.attachment
|
.attachment
|
||||||
|
|||||||
@@ -3,18 +3,19 @@ use chrono::{DateTime, Utc};
|
|||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
/// Read side — the library queries this when sending content outward.
|
/// Read side — the library queries this when sending content outward.
|
||||||
/// Implement on the same struct as [`ApObjectHandler`] if you prefer
|
/// Implement on the same struct as [`ApObjectHandler`] if you prefer a single
|
||||||
/// a single database type.
|
/// database type.
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
pub trait ApContentReader: Send + Sync {
|
pub trait ApContentReader: Send + Sync {
|
||||||
/// All locally-authored objects for this user. Used by backfill on accept_follower.
|
/// Newest-first page of locally-authored objects for `user_id`, published
|
||||||
async fn get_local_objects_for_user(
|
/// strictly before `before` (pass `None` for the first page).
|
||||||
&self,
|
/// Returns `(ap_id, object_json, published_at)` tuples.
|
||||||
user_id: uuid::Uuid,
|
///
|
||||||
) -> anyhow::Result<Vec<(Url, serde_json::Value)>>;
|
/// Used by the outbox endpoint and by backfill when a new follower is
|
||||||
|
/// accepted. Implementations MUST:
|
||||||
/// Newest-first page of locally-authored objects, published before `before`.
|
/// - Return objects in descending `published_at` order.
|
||||||
/// Returns `(ap_id, object_json, published_at)`. Used by the outbox endpoint.
|
/// - Exclude deleted and draft content.
|
||||||
|
/// - Be consistent across pages (no duplicates, no gaps).
|
||||||
async fn get_local_objects_page(
|
async fn get_local_objects_page(
|
||||||
&self,
|
&self,
|
||||||
user_id: uuid::Uuid,
|
user_id: uuid::Uuid,
|
||||||
@@ -27,8 +28,22 @@ pub trait ApContentReader: Send + Sync {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Write side — the library calls these when processing inbound AP activities.
|
/// Write side — the library calls these when processing inbound AP activities.
|
||||||
|
///
|
||||||
|
/// All methods are called after HTTP signature verification has passed.
|
||||||
|
/// Returning `Err` propagates a 500 back to the remote server, which will
|
||||||
|
/// trigger a retry from well-behaved implementations. Return `Ok(())` to
|
||||||
|
/// silently accept an activity you don't want to act on.
|
||||||
|
///
|
||||||
|
/// **Idempotency:** Methods may be called more than once for the same activity
|
||||||
|
/// (e.g. under a race during duplicate delivery). Implementations should be
|
||||||
|
/// idempotent — prefer upsert over insert.
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
pub trait ApObjectHandler: Send + Sync {
|
pub trait ApObjectHandler: Send + Sync {
|
||||||
|
/// A remote actor published new content.
|
||||||
|
///
|
||||||
|
/// `ap_id` is the stable URL of the object (e.g. the Note URL, not the
|
||||||
|
/// Create activity URL). Store or index the `object` JSON as appropriate
|
||||||
|
/// for your domain.
|
||||||
async fn on_create(
|
async fn on_create(
|
||||||
&self,
|
&self,
|
||||||
ap_id: &Url,
|
ap_id: &Url,
|
||||||
@@ -36,6 +51,10 @@ pub trait ApObjectHandler: Send + Sync {
|
|||||||
object: serde_json::Value,
|
object: serde_json::Value,
|
||||||
) -> anyhow::Result<()>;
|
) -> anyhow::Result<()>;
|
||||||
|
|
||||||
|
/// A remote actor edited existing content.
|
||||||
|
///
|
||||||
|
/// `ap_id` matches a previously received `on_create` call. Update the
|
||||||
|
/// stored object.
|
||||||
async fn on_update(
|
async fn on_update(
|
||||||
&self,
|
&self,
|
||||||
ap_id: &Url,
|
ap_id: &Url,
|
||||||
@@ -43,18 +62,42 @@ pub trait ApObjectHandler: Send + Sync {
|
|||||||
object: serde_json::Value,
|
object: serde_json::Value,
|
||||||
) -> anyhow::Result<()>;
|
) -> anyhow::Result<()>;
|
||||||
|
|
||||||
|
/// A remote actor deleted an object previously delivered via `on_create`.
|
||||||
async fn on_delete(&self, ap_id: &Url, actor_url: &Url) -> anyhow::Result<()>;
|
async fn on_delete(&self, ap_id: &Url, actor_url: &Url) -> anyhow::Result<()>;
|
||||||
|
|
||||||
|
/// A remote actor was deleted or has unfollowed all local users.
|
||||||
|
///
|
||||||
|
/// Remove all content and state associated with `actor_url` from local
|
||||||
|
/// storage. Called for `Delete(actor)` and for `Undo(Follow)`.
|
||||||
async fn on_actor_removed(&self, actor_url: &Url) -> anyhow::Result<()>;
|
async fn on_actor_removed(&self, actor_url: &Url) -> anyhow::Result<()>;
|
||||||
|
|
||||||
|
/// A remote actor liked a locally-authored object.
|
||||||
async fn on_like(&self, object_url: &Url, actor_url: &Url) -> anyhow::Result<()>;
|
async fn on_like(&self, object_url: &Url, actor_url: &Url) -> anyhow::Result<()>;
|
||||||
|
|
||||||
|
/// A remote actor removed their like from a locally-authored object.
|
||||||
async fn on_unlike(&self, object_url: &Url, actor_url: &Url) -> anyhow::Result<()>;
|
async fn on_unlike(&self, object_url: &Url, actor_url: &Url) -> anyhow::Result<()>;
|
||||||
|
|
||||||
|
/// A remote actor boosted (Announced) a **locally-authored** object.
|
||||||
|
///
|
||||||
|
/// `object_url` is your local object's AP URL. The boost count is tracked
|
||||||
|
/// separately in [`crate::repository::ActorRepository::count_announces`].
|
||||||
async fn on_announce_received(&self, object_url: &Url, actor_url: &Url) -> anyhow::Result<()>;
|
async fn on_announce_received(&self, object_url: &Url, actor_url: &Url) -> anyhow::Result<()>;
|
||||||
|
|
||||||
|
/// A remote actor boosted an object hosted on a **different server**.
|
||||||
|
///
|
||||||
|
/// Use this to surface cross-server boosts in local feeds. Called instead
|
||||||
|
/// of `on_announce_received` when the announced object URL is external.
|
||||||
|
/// Failures are logged and swallowed — they do not fail the activity.
|
||||||
async fn on_announce_of_remote(&self, object_url: &Url, actor_url: &Url) -> anyhow::Result<()>;
|
async fn on_announce_of_remote(&self, object_url: &Url, actor_url: &Url) -> anyhow::Result<()>;
|
||||||
|
|
||||||
|
/// A local user was tagged (Mentioned) in an inbound Create or Update.
|
||||||
|
///
|
||||||
|
/// Called for every `{"type":"Mention","href":"<local-actor-url>"}` tag
|
||||||
|
/// found in inbound activities. Use this to send in-app notifications.
|
||||||
|
/// The note content is also delivered independently via `on_create`.
|
||||||
|
///
|
||||||
|
/// Failures are logged and swallowed — a broken notification must not
|
||||||
|
/// cause the activity to be rejected.
|
||||||
async fn on_mention(
|
async fn on_mention(
|
||||||
&self,
|
&self,
|
||||||
thought_ap_id: &Url,
|
thought_ap_id: &Url,
|
||||||
|
|||||||
@@ -43,6 +43,16 @@ pub trait FollowRepository: Send + Sync {
|
|||||||
/// followers, excluding blocked actors/domains. DB-side filtering.
|
/// followers, excluding blocked actors/domains. DB-side filtering.
|
||||||
async fn get_accepted_follower_inboxes(&self, local_user_id: uuid::Uuid)
|
async fn get_accepted_follower_inboxes(&self, local_user_id: uuid::Uuid)
|
||||||
-> Result<Vec<String>>;
|
-> Result<Vec<String>>;
|
||||||
|
/// Count of accepted followers only. More efficient than loading all followers
|
||||||
|
/// and filtering in application memory.
|
||||||
|
async fn count_accepted_followers(&self, local_user_id: uuid::Uuid) -> Result<usize>;
|
||||||
|
/// Accepted followers page for display purposes. `offset` is 0-based.
|
||||||
|
async fn get_accepted_followers_page(
|
||||||
|
&self,
|
||||||
|
local_user_id: uuid::Uuid,
|
||||||
|
offset: u32,
|
||||||
|
limit: usize,
|
||||||
|
) -> Result<Vec<RemoteActor>>;
|
||||||
|
|
||||||
// ── Outbound following ──────────────────────────────────────────────────
|
// ── Outbound following ──────────────────────────────────────────────────
|
||||||
async fn add_following(
|
async fn add_following(
|
||||||
|
|||||||
@@ -8,7 +8,19 @@ use crate::{activities::CreateActivity, actors::get_local_actor, federation::ApF
|
|||||||
use super::{ActivityPubService, delivery::send_with_retry};
|
use super::{ActivityPubService, delivery::send_with_retry};
|
||||||
|
|
||||||
impl ActivityPubService {
|
impl ActivityPubService {
|
||||||
pub async fn backfill_outbox(&self, outbox_url: &str, actor_url: &str) -> anyhow::Result<()> {
|
/// Fetch a remote actor's outbox and import its content into the local instance.
|
||||||
|
///
|
||||||
|
/// This is for importing a **remote actor's history** — for example, when you want
|
||||||
|
/// to surface an account's past posts after a local user follows them. It fetches
|
||||||
|
/// pages from `outbox_url` and calls `ApObjectHandler::on_create` for each item.
|
||||||
|
///
|
||||||
|
/// This is distinct from [`ActivityPubService::run_backfill_for_follower`], which
|
||||||
|
/// sends **your** local content to a new follower's inbox.
|
||||||
|
pub async fn import_remote_outbox(
|
||||||
|
&self,
|
||||||
|
outbox_url: &str,
|
||||||
|
actor_url: &str,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
let client = reqwest::Client::builder()
|
let client = reqwest::Client::builder()
|
||||||
.timeout(std::time::Duration::from_secs(
|
.timeout(std::time::Duration::from_secs(
|
||||||
super::HTTP_FETCH_TIMEOUT_SECS,
|
super::HTTP_FETCH_TIMEOUT_SECS,
|
||||||
|
|||||||
@@ -226,21 +226,53 @@ impl ActivityPubService {
|
|||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Fan out a Create(Note) activity to accepted followers and any explicitly
|
||||||
|
/// mentioned actors.
|
||||||
|
///
|
||||||
|
/// `visibility` controls `to`/`cc` addressing and whether the note is public:
|
||||||
|
/// - `Public` / `FollowersOnly`: delivered to followers + `mentioned_inboxes`
|
||||||
|
/// - `Private`: returns immediately — no delivery to anyone
|
||||||
|
///
|
||||||
|
/// `mentioned_inboxes` should contain the inbox URLs of remote actors
|
||||||
|
/// explicitly tagged in the note who are not already followers. Resolve them
|
||||||
|
/// via [`ActivityPubService::lookup_actor_by_handle`] before calling. Pass an
|
||||||
|
/// empty `Vec` if there are no external mentions.
|
||||||
pub async fn broadcast_create_note(
|
pub async fn broadcast_create_note(
|
||||||
&self,
|
&self,
|
||||||
local_user_id: uuid::Uuid,
|
local_user_id: uuid::Uuid,
|
||||||
note: serde_json::Value,
|
note: serde_json::Value,
|
||||||
visibility: ApVisibility,
|
visibility: ApVisibility,
|
||||||
|
mentioned_inboxes: Vec<Url>,
|
||||||
) -> anyhow::Result<()> {
|
) -> anyhow::Result<()> {
|
||||||
if visibility == ApVisibility::Private {
|
if visibility == ApVisibility::Private {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
let data = self.federation_config.to_request_data();
|
let data = self.federation_config.to_request_data();
|
||||||
let Some((local_actor, inboxes)) =
|
let local_actor = crate::actors::get_local_actor(local_user_id, &data)
|
||||||
self.accepted_follower_inboxes(&data, local_user_id).await?
|
.await
|
||||||
else {
|
.map_err(|e| anyhow::anyhow!("{e}"))?;
|
||||||
|
|
||||||
|
// Merge follower inboxes with explicitly mentioned actor inboxes,
|
||||||
|
// deduplicating by string to avoid delivering the same inbox twice.
|
||||||
|
let follower_inboxes = data
|
||||||
|
.follow_repo
|
||||||
|
.get_accepted_follower_inboxes(local_user_id)
|
||||||
|
.await?;
|
||||||
|
let mut seen = std::collections::HashSet::new();
|
||||||
|
let mut inboxes: Vec<Url> = follower_inboxes
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|s| Url::parse(&s).ok())
|
||||||
|
.filter(|u| seen.insert(u.to_string()))
|
||||||
|
.collect();
|
||||||
|
for inbox in mentioned_inboxes {
|
||||||
|
if seen.insert(inbox.to_string()) {
|
||||||
|
inboxes.push(inbox);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if inboxes.is_empty() {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
};
|
}
|
||||||
|
|
||||||
let note_id_str = note["id"].as_str().unwrap_or("");
|
let note_id_str = note["id"].as_str().unwrap_or("");
|
||||||
let create_id = Url::parse(&format!(
|
let create_id = Url::parse(&format!(
|
||||||
"{}/activities/create/{}",
|
"{}/activities/create/{}",
|
||||||
@@ -266,21 +298,42 @@ impl ActivityPubService {
|
|||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Fan out an Update(Note) activity to accepted followers and mentioned actors.
|
||||||
|
/// See [`broadcast_create_note`] for `mentioned_inboxes` semantics.
|
||||||
pub async fn broadcast_update_note(
|
pub async fn broadcast_update_note(
|
||||||
&self,
|
&self,
|
||||||
local_user_id: uuid::Uuid,
|
local_user_id: uuid::Uuid,
|
||||||
note: serde_json::Value,
|
note: serde_json::Value,
|
||||||
visibility: ApVisibility,
|
visibility: ApVisibility,
|
||||||
|
mentioned_inboxes: Vec<Url>,
|
||||||
) -> anyhow::Result<()> {
|
) -> anyhow::Result<()> {
|
||||||
if visibility == ApVisibility::Private {
|
if visibility == ApVisibility::Private {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
let data = self.federation_config.to_request_data();
|
let data = self.federation_config.to_request_data();
|
||||||
let Some((local_actor, inboxes)) =
|
let local_actor = crate::actors::get_local_actor(local_user_id, &data)
|
||||||
self.accepted_follower_inboxes(&data, local_user_id).await?
|
.await
|
||||||
else {
|
.map_err(|e| anyhow::anyhow!("{e}"))?;
|
||||||
|
|
||||||
|
let follower_inboxes = data
|
||||||
|
.follow_repo
|
||||||
|
.get_accepted_follower_inboxes(local_user_id)
|
||||||
|
.await?;
|
||||||
|
let mut seen = std::collections::HashSet::new();
|
||||||
|
let mut inboxes: Vec<Url> = follower_inboxes
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|s| Url::parse(&s).ok())
|
||||||
|
.filter(|u| seen.insert(u.to_string()))
|
||||||
|
.collect();
|
||||||
|
for inbox in mentioned_inboxes {
|
||||||
|
if seen.insert(inbox.to_string()) {
|
||||||
|
inboxes.push(inbox);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if inboxes.is_empty() {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
};
|
}
|
||||||
|
|
||||||
let (to, cc) = visibility_addressing(visibility, &local_actor.followers_url);
|
let (to, cc) = visibility_addressing(visibility, &local_actor.followers_url);
|
||||||
let update = crate::activities::UpdateActivity {
|
let update = crate::activities::UpdateActivity {
|
||||||
id: activity_url(&self.base_url).map_err(|e| anyhow::anyhow!("{e}"))?,
|
id: activity_url(&self.base_url).map_err(|e| anyhow::anyhow!("{e}"))?,
|
||||||
|
|||||||
@@ -209,6 +209,21 @@ impl ActivityPubService {
|
|||||||
data.follow_repo.get_pending_followers(local_user_id).await
|
data.follow_repo.get_pending_followers(local_user_id).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns one page of accepted followers. Prefer this over `get_accepted_followers`
|
||||||
|
/// for large accounts — the DB does the filtering rather than loading everything.
|
||||||
|
pub async fn get_accepted_followers_page(
|
||||||
|
&self,
|
||||||
|
local_user_id: uuid::Uuid,
|
||||||
|
offset: u32,
|
||||||
|
limit: usize,
|
||||||
|
) -> anyhow::Result<Vec<RemoteActor>> {
|
||||||
|
let data = self.federation_config.to_request_data();
|
||||||
|
data.follow_repo
|
||||||
|
.get_accepted_followers_page(local_user_id, offset, limit)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns ALL accepted followers. For large accounts use `get_accepted_followers_page`.
|
||||||
pub async fn get_accepted_followers(
|
pub async fn get_accepted_followers(
|
||||||
&self,
|
&self,
|
||||||
local_user_id: uuid::Uuid,
|
local_user_id: uuid::Uuid,
|
||||||
@@ -224,18 +239,15 @@ impl ActivityPubService {
|
|||||||
.collect())
|
.collect())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Count of accepted followers — DB-side query, no in-memory filtering.
|
||||||
pub async fn count_accepted_followers(
|
pub async fn count_accepted_followers(
|
||||||
&self,
|
&self,
|
||||||
local_user_id: uuid::Uuid,
|
local_user_id: uuid::Uuid,
|
||||||
) -> anyhow::Result<usize> {
|
) -> anyhow::Result<usize> {
|
||||||
let data = self.federation_config.to_request_data();
|
let data = self.federation_config.to_request_data();
|
||||||
Ok(data
|
data.follow_repo
|
||||||
.follow_repo
|
.count_accepted_followers(local_user_id)
|
||||||
.get_followers(local_user_id)
|
.await
|
||||||
.await?
|
|
||||||
.into_iter()
|
|
||||||
.filter(|f| f.status == FollowerStatus::Accepted)
|
|
||||||
.count())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_following(
|
pub async fn get_following(
|
||||||
|
|||||||
@@ -88,6 +88,17 @@ impl FollowRepository for MemFollowRepo {
|
|||||||
async fn get_accepted_follower_inboxes(&self, _: uuid::Uuid) -> anyhow::Result<Vec<String>> {
|
async fn get_accepted_follower_inboxes(&self, _: uuid::Uuid) -> anyhow::Result<Vec<String>> {
|
||||||
Ok(vec![])
|
Ok(vec![])
|
||||||
}
|
}
|
||||||
|
async fn count_accepted_followers(&self, _: uuid::Uuid) -> anyhow::Result<usize> {
|
||||||
|
Ok(0)
|
||||||
|
}
|
||||||
|
async fn get_accepted_followers_page(
|
||||||
|
&self,
|
||||||
|
_: uuid::Uuid,
|
||||||
|
_: u32,
|
||||||
|
_: usize,
|
||||||
|
) -> anyhow::Result<Vec<RemoteActor>> {
|
||||||
|
Ok(vec![])
|
||||||
|
}
|
||||||
async fn add_following(&self, _: uuid::Uuid, _: RemoteActor, _: &str) -> anyhow::Result<()> {
|
async fn add_following(&self, _: uuid::Uuid, _: RemoteActor, _: &str) -> anyhow::Result<()> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -246,7 +257,7 @@ impl MemUserRepo {
|
|||||||
bio: None,
|
bio: None,
|
||||||
avatar_url: None,
|
avatar_url: None,
|
||||||
banner_url: None,
|
banner_url: None,
|
||||||
also_known_as: None,
|
also_known_as: vec![],
|
||||||
profile_url: None,
|
profile_url: None,
|
||||||
attachment: vec![],
|
attachment: vec![],
|
||||||
manually_approves_followers: true,
|
manually_approves_followers: true,
|
||||||
@@ -283,12 +294,6 @@ struct MemContentReader;
|
|||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl ApContentReader for MemContentReader {
|
impl ApContentReader for MemContentReader {
|
||||||
async fn get_local_objects_for_user(
|
|
||||||
&self,
|
|
||||||
_: uuid::Uuid,
|
|
||||||
) -> anyhow::Result<Vec<(Url, serde_json::Value)>> {
|
|
||||||
Ok(vec![])
|
|
||||||
}
|
|
||||||
async fn get_local_objects_page(
|
async fn get_local_objects_page(
|
||||||
&self,
|
&self,
|
||||||
_: uuid::Uuid,
|
_: uuid::Uuid,
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ pub struct LookedUpActor {
|
|||||||
pub outbox_url: Option<Url>,
|
pub outbox_url: Option<Url>,
|
||||||
pub followers_url: Option<Url>,
|
pub followers_url: Option<Url>,
|
||||||
pub following_url: Option<Url>,
|
pub following_url: Option<Url>,
|
||||||
pub also_known_as: Option<String>,
|
pub also_known_as: Vec<String>,
|
||||||
pub profile_url: Option<Url>,
|
pub profile_url: Option<Url>,
|
||||||
pub attachment: Vec<ApProfileField>,
|
pub attachment: Vec<ApProfileField>,
|
||||||
}
|
}
|
||||||
@@ -61,7 +61,7 @@ pub struct ApUser {
|
|||||||
pub bio: Option<String>,
|
pub bio: Option<String>,
|
||||||
pub avatar_url: Option<Url>,
|
pub avatar_url: Option<Url>,
|
||||||
pub banner_url: Option<Url>,
|
pub banner_url: Option<Url>,
|
||||||
pub also_known_as: Option<String>,
|
pub also_known_as: Vec<String>,
|
||||||
pub profile_url: Option<Url>,
|
pub profile_url: Option<Url>,
|
||||||
pub attachment: Vec<ApProfileField>,
|
pub attachment: Vec<ApProfileField>,
|
||||||
/// If true, incoming Follow requests must be manually approved before the
|
/// If true, incoming Follow requests must be manually approved before the
|
||||||
|
|||||||
Reference in New Issue
Block a user