feat(domain): add list_mutual to FollowRepository, add remote actor storage to TestStore
This commit is contained in:
@@ -187,6 +187,50 @@ impl FollowRepository for PgFollowRepository {
|
|||||||
.into_domain()?;
|
.into_domain()?;
|
||||||
Ok(ids.into_iter().map(UserId::from_uuid).collect())
|
Ok(ids.into_iter().map(UserId::from_uuid).collect())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn list_mutual(
|
||||||
|
&self,
|
||||||
|
user_id: &UserId,
|
||||||
|
page: &PageParams,
|
||||||
|
) -> Result<Paginated<User>, DomainError> {
|
||||||
|
let total: i64 = sqlx::query_scalar(
|
||||||
|
"SELECT COUNT(DISTINCT u.id)
|
||||||
|
FROM users u
|
||||||
|
WHERE EXISTS (
|
||||||
|
SELECT 1 FROM follows f1 WHERE f1.follower_id=$1 AND f1.following_id=u.id AND f1.state='accepted'
|
||||||
|
) AND EXISTS (
|
||||||
|
SELECT 1 FROM follows f2 WHERE f2.following_id=$1 AND f2.follower_id=u.id AND f2.state='accepted'
|
||||||
|
)",
|
||||||
|
)
|
||||||
|
.bind(user_id.as_uuid())
|
||||||
|
.fetch_one(&self.pool)
|
||||||
|
.await
|
||||||
|
.into_domain()?;
|
||||||
|
|
||||||
|
let rows = sqlx::query_as::<_, crate::user::UserRow>(
|
||||||
|
"SELECT u.id,u.username,u.email,u.password_hash,u.display_name,u.bio,u.avatar_url,u.header_url,u.custom_css,u.local,u.ap_id,u.inbox_url,u.created_at,u.updated_at
|
||||||
|
FROM users u
|
||||||
|
WHERE EXISTS (
|
||||||
|
SELECT 1 FROM follows f1 WHERE f1.follower_id=$1 AND f1.following_id=u.id AND f1.state='accepted'
|
||||||
|
) AND EXISTS (
|
||||||
|
SELECT 1 FROM follows f2 WHERE f2.following_id=$1 AND f2.follower_id=u.id AND f2.state='accepted'
|
||||||
|
)
|
||||||
|
ORDER BY u.created_at DESC LIMIT $2 OFFSET $3"
|
||||||
|
)
|
||||||
|
.bind(user_id.as_uuid())
|
||||||
|
.bind(page.limit())
|
||||||
|
.bind(page.offset())
|
||||||
|
.fetch_all(&self.pool)
|
||||||
|
.await
|
||||||
|
.into_domain()?;
|
||||||
|
|
||||||
|
Ok(Paginated {
|
||||||
|
items: rows.into_iter().map(User::from).collect(),
|
||||||
|
total,
|
||||||
|
page: page.page,
|
||||||
|
per_page: page.per_page,
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|||||||
@@ -171,6 +171,11 @@ pub trait FollowRepository: Send + Sync {
|
|||||||
&self,
|
&self,
|
||||||
user_id: &UserId,
|
user_id: &UserId,
|
||||||
) -> Result<Vec<UserId>, DomainError>;
|
) -> Result<Vec<UserId>, DomainError>;
|
||||||
|
async fn list_mutual(
|
||||||
|
&self,
|
||||||
|
user_id: &UserId,
|
||||||
|
page: &PageParams,
|
||||||
|
) -> Result<Paginated<User>, DomainError>;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
|
|||||||
@@ -37,6 +37,8 @@ pub struct TestStore {
|
|||||||
pub actor_ap_ids: Arc<Mutex<HashMap<String, UserId>>>,
|
pub actor_ap_ids: Arc<Mutex<HashMap<String, UserId>>>,
|
||||||
/// ThoughtId → AP object URL (used by get_thought_ap_id)
|
/// ThoughtId → AP object URL (used by get_thought_ap_id)
|
||||||
pub thought_ap_ids: Arc<Mutex<HashMap<ThoughtId, String>>>,
|
pub thought_ap_ids: Arc<Mutex<HashMap<ThoughtId, String>>>,
|
||||||
|
pub remote_following: Arc<Mutex<Vec<RemoteActor>>>,
|
||||||
|
pub remote_followers: Arc<Mutex<Vec<RemoteActor>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
@@ -452,6 +454,40 @@ impl FollowRepository for TestStore {
|
|||||||
.map(|f| f.following_id.clone())
|
.map(|f| f.following_id.clone())
|
||||||
.collect())
|
.collect())
|
||||||
}
|
}
|
||||||
|
async fn list_mutual(
|
||||||
|
&self,
|
||||||
|
user_id: &UserId,
|
||||||
|
page: &PageParams,
|
||||||
|
) -> Result<Paginated<User>, DomainError> {
|
||||||
|
use std::collections::HashSet;
|
||||||
|
let follows = self.follows.lock().unwrap();
|
||||||
|
let following_ids: HashSet<UserId> = follows
|
||||||
|
.iter()
|
||||||
|
.filter(|f| &f.follower_id == user_id && f.state == FollowState::Accepted)
|
||||||
|
.map(|f| f.following_id.clone())
|
||||||
|
.collect();
|
||||||
|
let follower_ids: HashSet<UserId> = follows
|
||||||
|
.iter()
|
||||||
|
.filter(|f| &f.following_id == user_id && f.state == FollowState::Accepted)
|
||||||
|
.map(|f| f.follower_id.clone())
|
||||||
|
.collect();
|
||||||
|
let mutual_ids: HashSet<UserId> =
|
||||||
|
following_ids.intersection(&follower_ids).cloned().collect();
|
||||||
|
drop(follows);
|
||||||
|
let users = self.users.lock().unwrap();
|
||||||
|
let items: Vec<User> = users
|
||||||
|
.iter()
|
||||||
|
.filter(|u| mutual_ids.contains(&u.id))
|
||||||
|
.cloned()
|
||||||
|
.collect();
|
||||||
|
let total = items.len() as i64;
|
||||||
|
Ok(Paginated {
|
||||||
|
items,
|
||||||
|
total,
|
||||||
|
page: page.page,
|
||||||
|
per_page: page.per_page,
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
@@ -710,7 +746,7 @@ impl FederationFollowPort for TestStore {
|
|||||||
&self,
|
&self,
|
||||||
_user_id: &UserId,
|
_user_id: &UserId,
|
||||||
) -> Result<Vec<RemoteActor>, DomainError> {
|
) -> Result<Vec<RemoteActor>, DomainError> {
|
||||||
Ok(vec![])
|
Ok(self.remote_following.lock().unwrap().clone())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn broadcast_move(
|
async fn broadcast_move(
|
||||||
@@ -751,7 +787,7 @@ impl FederationFollowRequestPort for TestStore {
|
|||||||
&self,
|
&self,
|
||||||
_user_id: &UserId,
|
_user_id: &UserId,
|
||||||
) -> Result<Vec<RemoteActor>, DomainError> {
|
) -> Result<Vec<RemoteActor>, DomainError> {
|
||||||
Ok(vec![])
|
Ok(self.remote_followers.lock().unwrap().clone())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn remove_remote_follower(
|
async fn remove_remote_follower(
|
||||||
|
|||||||
Reference in New Issue
Block a user