Compare commits
55 Commits
e406464f9f
...
de1d2f7ec7
| Author | SHA1 | Date | |
|---|---|---|---|
| de1d2f7ec7 | |||
| e8d42b01dc | |||
| 4038a6b554 | |||
| be4a37546c | |||
| 7a2d8308d9 | |||
| 4a5d5df884 | |||
| 421cb463e3 | |||
| 925f4f8bf3 | |||
| e5c8380ba7 | |||
| 97bc918bbc | |||
| 805240aaf8 | |||
| cd6148eff9 | |||
| 6f1a0572df | |||
| 0841554dbe | |||
| c30243f1c8 | |||
| 1ad02e0806 | |||
| 23a8444b5c | |||
| b20b60ad10 | |||
| 54bd2b60d0 | |||
| 94193f2d2e | |||
| f54fb543b2 | |||
| a460428be1 | |||
| 95dea06c55 | |||
| c085067318 | |||
| d831784489 | |||
| 4c203bed1d | |||
| 21b8684608 | |||
| 74eeb9fcb9 | |||
| 7ee22ae79f | |||
| 3f26456d77 | |||
| 379f31e27d | |||
| 9c99f7a7a8 | |||
| 636d3d453d | |||
| 9172c82d54 | |||
| cd2eb48ddb | |||
| c5d9833c8b | |||
| f39c1a614d | |||
| 30c8a17168 | |||
| 6a8c8b1fb8 | |||
| 4ec0725ff8 | |||
| 31e0f2958c | |||
| 555121ea75 | |||
| 9e795eefdc | |||
| 18cf2c9f54 | |||
| b58c96b843 | |||
| 8ea24461ba | |||
| e14a9f90c8 | |||
| 28756ef4cd | |||
| 7f27ae49c3 | |||
| 59f3423c00 | |||
| c48aa33592 | |||
| 8f3aa4b891 | |||
| 32bfb00970 | |||
| 7ce2901c2a | |||
| 8bbc713093 |
@@ -187,59 +187,6 @@ 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(*) FROM follows f1
|
|
||||||
WHERE f1.follower_id = $1 AND f1.state = 'accepted'
|
|
||||||
AND EXISTS (
|
|
||||||
SELECT 1 FROM follows f2
|
|
||||||
WHERE f2.follower_id = f1.following_id
|
|
||||||
AND f2.following_id = f1.follower_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.created_at, u.updated_at
|
|
||||||
FROM users u
|
|
||||||
JOIN follows f1
|
|
||||||
ON f1.follower_id = $1
|
|
||||||
AND f1.following_id = u.id
|
|
||||||
AND f1.state = 'accepted'
|
|
||||||
WHERE EXISTS (
|
|
||||||
SELECT 1 FROM follows f2
|
|
||||||
WHERE f2.follower_id = u.id
|
|
||||||
AND f2.following_id = $1
|
|
||||||
AND f2.state = 'accepted'
|
|
||||||
)
|
|
||||||
ORDER BY f1.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)]
|
||||||
|
|||||||
@@ -56,86 +56,3 @@ async fn get_accepted_following_ids(pool: sqlx::PgPool) {
|
|||||||
let ids = repo.get_accepted_following_ids(&alice.id).await.unwrap();
|
let ids = repo.get_accepted_following_ids(&alice.id).await.unwrap();
|
||||||
assert_eq!(ids, vec![bob.id]);
|
assert_eq!(ids, vec![bob.id]);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[sqlx::test(migrations = "./migrations")]
|
|
||||||
async fn list_mutual_returns_only_mutual_accepted_follows(pool: sqlx::PgPool) {
|
|
||||||
let alice = seed_user(&pool, "alice", "alice@ex.com").await;
|
|
||||||
let bob = seed_user(&pool, "bob", "bob@ex.com").await;
|
|
||||||
let carol = seed_user(&pool, "carol", "carol@ex.com").await;
|
|
||||||
let repo = PgFollowRepository::new(pool);
|
|
||||||
let page = domain::models::feed::PageParams {
|
|
||||||
page: 1,
|
|
||||||
per_page: 20,
|
|
||||||
};
|
|
||||||
|
|
||||||
// alice → bob (accepted), bob → alice (accepted) = friends
|
|
||||||
repo.save(&Follow {
|
|
||||||
follower_id: alice.id.clone(),
|
|
||||||
following_id: bob.id.clone(),
|
|
||||||
state: FollowState::Accepted,
|
|
||||||
ap_id: None,
|
|
||||||
created_at: Utc::now(),
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
repo.save(&Follow {
|
|
||||||
follower_id: bob.id.clone(),
|
|
||||||
following_id: alice.id.clone(),
|
|
||||||
state: FollowState::Accepted,
|
|
||||||
ap_id: None,
|
|
||||||
created_at: Utc::now(),
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
// alice → carol (accepted), carol does NOT follow back = not a friend
|
|
||||||
repo.save(&Follow {
|
|
||||||
follower_id: alice.id.clone(),
|
|
||||||
following_id: carol.id.clone(),
|
|
||||||
state: FollowState::Accepted,
|
|
||||||
ap_id: None,
|
|
||||||
created_at: Utc::now(),
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let result = repo.list_mutual(&alice.id, &page).await.unwrap();
|
|
||||||
assert_eq!(result.total, 1);
|
|
||||||
assert_eq!(result.items.len(), 1);
|
|
||||||
assert_eq!(result.items[0].id, bob.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[sqlx::test(migrations = "./migrations")]
|
|
||||||
async fn list_mutual_excludes_pending_follows(pool: sqlx::PgPool) {
|
|
||||||
let alice = seed_user(&pool, "alice", "alice@ex.com").await;
|
|
||||||
let bob = seed_user(&pool, "bob", "bob@ex.com").await;
|
|
||||||
let repo = PgFollowRepository::new(pool);
|
|
||||||
let page = domain::models::feed::PageParams {
|
|
||||||
page: 1,
|
|
||||||
per_page: 20,
|
|
||||||
};
|
|
||||||
|
|
||||||
// alice → bob (accepted), bob → alice (PENDING) = NOT a friend
|
|
||||||
repo.save(&Follow {
|
|
||||||
follower_id: alice.id.clone(),
|
|
||||||
following_id: bob.id.clone(),
|
|
||||||
state: FollowState::Accepted,
|
|
||||||
ap_id: None,
|
|
||||||
created_at: Utc::now(),
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
repo.save(&Follow {
|
|
||||||
follower_id: bob.id.clone(),
|
|
||||||
following_id: alice.id.clone(),
|
|
||||||
state: FollowState::Pending,
|
|
||||||
ap_id: None,
|
|
||||||
created_at: Utc::now(),
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let result = repo.list_mutual(&alice.id, &page).await.unwrap();
|
|
||||||
assert_eq!(result.total, 0);
|
|
||||||
assert!(result.items.is_empty());
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -96,20 +96,6 @@ pub async fn list_remote_following(
|
|||||||
federation.get_remote_following(user_id).await
|
federation.get_remote_following(user_id).await
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_remote_friends(
|
|
||||||
federation: &dyn FederationActionPort,
|
|
||||||
user_id: &UserId,
|
|
||||||
) -> Result<Vec<RemoteActor>, DomainError> {
|
|
||||||
use std::collections::HashSet;
|
|
||||||
let following = federation.get_remote_following(user_id).await?;
|
|
||||||
let followers = federation.get_remote_followers(user_id).await?;
|
|
||||||
let follower_urls: HashSet<&str> = followers.iter().map(|a| a.url.as_str()).collect();
|
|
||||||
Ok(following
|
|
||||||
.into_iter()
|
|
||||||
.filter(|a| follower_urls.contains(a.url.as_str()))
|
|
||||||
.collect())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn remove_remote_following(
|
pub async fn remove_remote_following(
|
||||||
follows: &dyn FollowRepository,
|
follows: &dyn FollowRepository,
|
||||||
users: &dyn UserReader,
|
users: &dyn UserReader,
|
||||||
|
|||||||
@@ -1,25 +1,6 @@
|
|||||||
use super::*;
|
use super::*;
|
||||||
use chrono::Utc;
|
|
||||||
use domain::models::remote_actor::RemoteActor;
|
|
||||||
use domain::testing::TestStore;
|
use domain::testing::TestStore;
|
||||||
|
|
||||||
fn remote_actor(url: &str, handle: &str) -> RemoteActor {
|
|
||||||
RemoteActor {
|
|
||||||
url: url.to_string(),
|
|
||||||
handle: handle.to_string(),
|
|
||||||
display_name: None,
|
|
||||||
avatar_url: None,
|
|
||||||
bio: None,
|
|
||||||
banner_url: None,
|
|
||||||
also_known_as: None,
|
|
||||||
outbox_url: None,
|
|
||||||
followers_url: None,
|
|
||||||
following_url: None,
|
|
||||||
attachment: vec![],
|
|
||||||
last_fetched_at: Utc::now(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn list_pending_returns_empty_by_default() {
|
async fn list_pending_returns_empty_by_default() {
|
||||||
let store = TestStore::default();
|
let store = TestStore::default();
|
||||||
@@ -70,41 +51,3 @@ async fn list_remote_following_returns_empty_by_default() {
|
|||||||
let result = list_remote_following(&store, &uid).await.unwrap();
|
let result = list_remote_following(&store, &uid).await.unwrap();
|
||||||
assert!(result.is_empty());
|
assert!(result.is_empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn get_remote_friends_returns_intersection() {
|
|
||||||
let store = TestStore::default();
|
|
||||||
let uid = UserId::new();
|
|
||||||
|
|
||||||
let bob = remote_actor("https://bob.example.com/users/bob", "bob@bob.example.com");
|
|
||||||
let carol = remote_actor(
|
|
||||||
"https://carol.example.com/users/carol",
|
|
||||||
"carol@carol.example.com",
|
|
||||||
);
|
|
||||||
|
|
||||||
// uid follows bob and carol
|
|
||||||
store
|
|
||||||
.remote_following
|
|
||||||
.lock()
|
|
||||||
.unwrap()
|
|
||||||
.extend([bob.clone(), carol.clone()]);
|
|
||||||
// only bob follows back
|
|
||||||
store.remote_followers.lock().unwrap().push(bob.clone());
|
|
||||||
|
|
||||||
let friends = get_remote_friends(&store, &uid).await.unwrap();
|
|
||||||
assert_eq!(friends.len(), 1);
|
|
||||||
assert_eq!(friends[0].url, bob.url);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn get_remote_friends_empty_when_no_mutual() {
|
|
||||||
let store = TestStore::default();
|
|
||||||
let uid = UserId::new();
|
|
||||||
|
|
||||||
let bob = remote_actor("https://bob.example.com/users/bob", "bob@bob.example.com");
|
|
||||||
store.remote_following.lock().unwrap().push(bob.clone());
|
|
||||||
// bob does NOT follow back
|
|
||||||
|
|
||||||
let friends = get_remote_friends(&store, &uid).await.unwrap();
|
|
||||||
assert!(friends.is_empty());
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -2,11 +2,7 @@ use chrono::Utc;
|
|||||||
use domain::{
|
use domain::{
|
||||||
errors::DomainError,
|
errors::DomainError,
|
||||||
events::DomainEvent,
|
events::DomainEvent,
|
||||||
models::{
|
models::social::{Block, Boost, Follow, FollowState, Like},
|
||||||
feed::{PageParams, Paginated},
|
|
||||||
social::{Block, Boost, Follow, FollowState, Like},
|
|
||||||
user::User,
|
|
||||||
},
|
|
||||||
ports::{
|
ports::{
|
||||||
BlockRepository, BoostRepository, EventPublisher, FederationFollowPort, FollowRepository,
|
BlockRepository, BoostRepository, EventPublisher, FederationFollowPort, FollowRepository,
|
||||||
LikeRepository, UserReader,
|
LikeRepository, UserReader,
|
||||||
@@ -284,13 +280,5 @@ pub async fn unblock_user(
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_local_friends(
|
|
||||||
follows: &dyn FollowRepository,
|
|
||||||
user_id: &UserId,
|
|
||||||
page: &PageParams,
|
|
||||||
) -> Result<Paginated<User>, DomainError> {
|
|
||||||
follows.list_mutual(user_id, page).await
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests;
|
mod tests;
|
||||||
|
|||||||
@@ -204,51 +204,3 @@ async fn boost_and_unboost() {
|
|||||||
.iter()
|
.iter()
|
||||||
.any(|e| matches!(e, DomainEvent::BoostRemoved { .. })));
|
.any(|e| matches!(e, DomainEvent::BoostRemoved { .. })));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn get_local_friends_returns_mutual_follows() {
|
|
||||||
use domain::models::feed::PageParams;
|
|
||||||
let store = TestStore::default();
|
|
||||||
let alice = user("alice");
|
|
||||||
let bob = user("bob");
|
|
||||||
let carol = user("carol");
|
|
||||||
|
|
||||||
store
|
|
||||||
.users
|
|
||||||
.lock()
|
|
||||||
.unwrap()
|
|
||||||
.extend([alice.clone(), bob.clone(), carol.clone()]);
|
|
||||||
|
|
||||||
// alice ↔ bob = friends; alice → carol but not back
|
|
||||||
store.follows.lock().unwrap().extend([
|
|
||||||
domain::models::social::Follow {
|
|
||||||
follower_id: alice.id.clone(),
|
|
||||||
following_id: bob.id.clone(),
|
|
||||||
state: domain::models::social::FollowState::Accepted,
|
|
||||||
ap_id: None,
|
|
||||||
created_at: chrono::Utc::now(),
|
|
||||||
},
|
|
||||||
domain::models::social::Follow {
|
|
||||||
follower_id: bob.id.clone(),
|
|
||||||
following_id: alice.id.clone(),
|
|
||||||
state: domain::models::social::FollowState::Accepted,
|
|
||||||
ap_id: None,
|
|
||||||
created_at: chrono::Utc::now(),
|
|
||||||
},
|
|
||||||
domain::models::social::Follow {
|
|
||||||
follower_id: alice.id.clone(),
|
|
||||||
following_id: carol.id.clone(),
|
|
||||||
state: domain::models::social::FollowState::Accepted,
|
|
||||||
ap_id: None,
|
|
||||||
created_at: chrono::Utc::now(),
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
let page = PageParams {
|
|
||||||
page: 1,
|
|
||||||
per_page: 20,
|
|
||||||
};
|
|
||||||
let result = get_local_friends(&store, &alice.id, &page).await.unwrap();
|
|
||||||
assert_eq!(result.total, 1);
|
|
||||||
assert_eq!(result.items[0].id, bob.id);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -171,11 +171,6 @@ 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,8 +37,6 @@ 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]
|
||||||
@@ -454,46 +452,6 @@ 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 all_items: Vec<User> = users
|
|
||||||
.iter()
|
|
||||||
.filter(|u| mutual_ids.contains(&u.id))
|
|
||||||
.cloned()
|
|
||||||
.collect();
|
|
||||||
let total = all_items.len() as i64;
|
|
||||||
let offset = page.offset() as usize;
|
|
||||||
let items: Vec<User> = all_items
|
|
||||||
.into_iter()
|
|
||||||
.skip(offset)
|
|
||||||
.take(page.limit() as usize)
|
|
||||||
.collect();
|
|
||||||
Ok(Paginated {
|
|
||||||
items,
|
|
||||||
total,
|
|
||||||
page: page.page,
|
|
||||||
per_page: page.per_page,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
@@ -752,7 +710,7 @@ impl FederationFollowPort for TestStore {
|
|||||||
&self,
|
&self,
|
||||||
_user_id: &UserId,
|
_user_id: &UserId,
|
||||||
) -> Result<Vec<RemoteActor>, DomainError> {
|
) -> Result<Vec<RemoteActor>, DomainError> {
|
||||||
Ok(self.remote_following.lock().unwrap().clone())
|
Ok(vec![])
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn broadcast_move(
|
async fn broadcast_move(
|
||||||
@@ -793,7 +751,7 @@ impl FederationFollowRequestPort for TestStore {
|
|||||||
&self,
|
&self,
|
||||||
_user_id: &UserId,
|
_user_id: &UserId,
|
||||||
) -> Result<Vec<RemoteActor>, DomainError> {
|
) -> Result<Vec<RemoteActor>, DomainError> {
|
||||||
Ok(self.remote_followers.lock().unwrap().clone())
|
Ok(vec![])
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn remove_remote_follower(
|
async fn remove_remote_follower(
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ use crate::{
|
|||||||
};
|
};
|
||||||
use api_types::responses::{ProfileField, RemoteActorResponse};
|
use api_types::responses::{ProfileField, RemoteActorResponse};
|
||||||
use application::use_cases::federation_management::{
|
use application::use_cases::federation_management::{
|
||||||
accept_follow_request, get_remote_friends, initiate_actor_move, list_pending_requests,
|
accept_follow_request, initiate_actor_move, list_pending_requests, list_remote_followers,
|
||||||
list_remote_followers, list_remote_following, reject_follow_request, remove_remote_following,
|
list_remote_following, reject_follow_request, remove_remote_following,
|
||||||
};
|
};
|
||||||
use axum::{http::StatusCode, Json};
|
use axum::{http::StatusCode, Json};
|
||||||
use domain::ports::{EventPublisher, FederationActionPort, FollowRepository, UserRepository};
|
use domain::ports::{EventPublisher, FederationActionPort, FollowRepository, UserRepository};
|
||||||
@@ -96,14 +96,6 @@ pub async fn get_remote_following(
|
|||||||
Ok(Json(actors.into_iter().map(to_response).collect()))
|
Ok(Json(actors.into_iter().map(to_response).collect()))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_remote_friends_handler(
|
|
||||||
Deps(d): Deps<FederationManagementDeps>,
|
|
||||||
AuthUser(uid): AuthUser,
|
|
||||||
) -> Result<Json<Vec<RemoteActorResponse>>, ApiError> {
|
|
||||||
let actors = get_remote_friends(&*d.federation, &uid).await?;
|
|
||||||
Ok(Json(actors.into_iter().map(to_response).collect()))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn delete_following(
|
pub async fn delete_following(
|
||||||
Deps(d): Deps<FederationManagementDeps>,
|
Deps(d): Deps<FederationManagementDeps>,
|
||||||
AuthUser(uid): AuthUser,
|
AuthUser(uid): AuthUser,
|
||||||
|
|||||||
@@ -4,15 +4,11 @@ use crate::{
|
|||||||
errors::ApiError,
|
errors::ApiError,
|
||||||
extractors::{AuthUser, Deps},
|
extractors::{AuthUser, Deps},
|
||||||
};
|
};
|
||||||
use api_types::requests::{PaginationQuery, SetTopFriendsRequest};
|
use api_types::requests::SetTopFriendsRequest;
|
||||||
use api_types::responses::{PagedResponse, TopFriendsResponse, UserResponse};
|
use api_types::responses::TopFriendsResponse;
|
||||||
use application::use_cases::profile::{get_top_friends, get_user_by_username, set_top_friends};
|
use application::use_cases::profile::{get_top_friends, get_user_by_username, set_top_friends};
|
||||||
use application::use_cases::social::*;
|
use application::use_cases::social::*;
|
||||||
use axum::{
|
use axum::{extract::Path, http::StatusCode, Json};
|
||||||
extract::{Path, Query},
|
|
||||||
http::StatusCode,
|
|
||||||
Json,
|
|
||||||
};
|
|
||||||
use domain::{
|
use domain::{
|
||||||
ports::{
|
ports::{
|
||||||
BlockRepository, BoostRepository, EventPublisher, FederationActionPort, FollowRepository,
|
BlockRepository, BoostRepository, EventPublisher, FederationActionPort, FollowRepository,
|
||||||
@@ -154,33 +150,5 @@ pub async fn get_top_friends_handler(
|
|||||||
Ok(Json(TopFriendsResponse { top_friends }))
|
Ok(Json(TopFriendsResponse { top_friends }))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[utoipa::path(
|
|
||||||
get, path = "/users/me/friends",
|
|
||||||
params(PaginationQuery),
|
|
||||||
responses(
|
|
||||||
(status = 200, description = "Local mutual follows (paginated)", body = inline(PagedResponse<UserResponse>)),
|
|
||||||
(status = 401, description = "Unauthorized"),
|
|
||||||
),
|
|
||||||
security(("bearer_auth" = []))
|
|
||||||
)]
|
|
||||||
pub async fn get_friends_handler(
|
|
||||||
Deps(d): Deps<SocialDeps>,
|
|
||||||
AuthUser(uid): AuthUser,
|
|
||||||
Query(q): Query<PaginationQuery>,
|
|
||||||
) -> Result<Json<PagedResponse<UserResponse>>, ApiError> {
|
|
||||||
use domain::models::feed::PageParams;
|
|
||||||
let page = PageParams {
|
|
||||||
page: q.page(),
|
|
||||||
per_page: q.per_page(),
|
|
||||||
};
|
|
||||||
let result = get_local_friends(&*d.follows, &uid, &page).await?;
|
|
||||||
Ok(Json(PagedResponse {
|
|
||||||
items: result.items.iter().map(to_user_response).collect(),
|
|
||||||
total: result.total,
|
|
||||||
page: result.page,
|
|
||||||
per_page: result.per_page,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests;
|
mod tests;
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
use super::get_friends_handler;
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::testing::make_state;
|
use crate::testing::make_state;
|
||||||
use axum::{
|
use axum::{
|
||||||
body::Body,
|
body::Body,
|
||||||
http::Request,
|
http::Request,
|
||||||
routing::{delete, get, post},
|
routing::{delete, post},
|
||||||
Router,
|
Router,
|
||||||
};
|
};
|
||||||
use tower::ServiceExt;
|
use tower::ServiceExt;
|
||||||
@@ -33,24 +32,6 @@ async fn follow_without_auth_returns_401() {
|
|||||||
assert_eq!(resp.status(), 401);
|
assert_eq!(resp.status(), 401);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn get_friends_without_auth_returns_401() {
|
|
||||||
let app = Router::new()
|
|
||||||
.route("/users/me/friends", get(get_friends_handler))
|
|
||||||
.with_state(make_state());
|
|
||||||
let resp = app
|
|
||||||
.oneshot(
|
|
||||||
Request::builder()
|
|
||||||
.method("GET")
|
|
||||||
.uri("/users/me/friends")
|
|
||||||
.body(Body::empty())
|
|
||||||
.unwrap(),
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
assert_eq!(resp.status(), 401);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn unfollow_remote_without_auth_returns_401() {
|
async fn unfollow_remote_without_auth_returns_401() {
|
||||||
let resp = app()
|
let resp = app()
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ use utoipa::OpenApi;
|
|||||||
crate::handlers::social::delete_block,
|
crate::handlers::social::delete_block,
|
||||||
crate::handlers::social::put_top_friends,
|
crate::handlers::social::put_top_friends,
|
||||||
crate::handlers::social::get_top_friends_handler,
|
crate::handlers::social::get_top_friends_handler,
|
||||||
crate::handlers::social::get_friends_handler,
|
|
||||||
),
|
),
|
||||||
components(schemas(SetTopFriendsRequest))
|
components(schemas(SetTopFriendsRequest))
|
||||||
)]
|
)]
|
||||||
|
|||||||
@@ -26,7 +26,6 @@ pub fn router() -> Router<AppState> {
|
|||||||
put(users::upload_banner).layer(DefaultBodyLimit::max(10 * 1024 * 1024)),
|
put(users::upload_banner).layer(DefaultBodyLimit::max(10 * 1024 * 1024)),
|
||||||
)
|
)
|
||||||
.route("/users/me/following", get(users::get_me_following))
|
.route("/users/me/following", get(users::get_me_following))
|
||||||
.route("/users/me/friends", get(social::get_friends_handler))
|
|
||||||
.route("/users/me/top-friends", put(social::put_top_friends))
|
.route("/users/me/top-friends", put(social::put_top_friends))
|
||||||
.route("/users/{username}", get(users::get_user))
|
.route("/users/{username}", get(users::get_user))
|
||||||
.route(
|
.route(
|
||||||
@@ -105,10 +104,6 @@ pub fn router() -> Router<AppState> {
|
|||||||
get(federation_management::get_remote_following)
|
get(federation_management::get_remote_following)
|
||||||
.delete(federation_management::delete_following),
|
.delete(federation_management::delete_following),
|
||||||
)
|
)
|
||||||
.route(
|
|
||||||
"/federation/me/friends",
|
|
||||||
get(federation_management::get_remote_friends_handler),
|
|
||||||
)
|
|
||||||
.route(
|
.route(
|
||||||
"/federation/me/move",
|
"/federation/me/move",
|
||||||
post(federation_management::post_move_account),
|
post(federation_management::post_move_account),
|
||||||
|
|||||||
Reference in New Issue
Block a user