Compare commits

..

55 Commits

Author SHA1 Message Date
de1d2f7ec7 fix(clippy): remove unused PasswordHash import
Some checks failed
lint / lint (push) Failing after 8m45s
test / unit (push) Successful in 16m15s
2026-05-28 03:00:54 +02:00
e8d42b01dc fix: look up remote parent by ap_id to thread remote-to-remote replies
Some checks failed
lint / lint (push) Has been cancelled
test / unit (push) Has been cancelled
2026-05-28 02:59:45 +02:00
4038a6b554 ci: remove failing integration tests job
Some checks failed
lint / lint (push) Failing after 7m13s
test / unit (push) Has been cancelled
2026-05-28 02:48:21 +02:00
be4a37546c refactor: delegate mark_follower_accepted/rejected through k-ap service, remove federation_repo from ApFederationAdapter
Some checks failed
test / unit (push) Has been cancelled
test / integration (push) Has been cancelled
lint / lint (push) Has been cancelled
2026-05-28 02:45:59 +02:00
7a2d8308d9 fix: extract initiate_actor_move use case — remove event publish from handler 2026-05-28 02:41:42 +02:00
4a5d5df884 fix(tests): update federation_management tests for EventPublisher arg 2026-05-28 02:34:30 +02:00
421cb463e3 feat: split accept/reject into DB+event; broadcast_move via event in API 2026-05-28 02:32:50 +02:00
925f4f8bf3 feat(worker): add FederationManagementHandler and wire into event loop 2026-05-28 02:30:22 +02:00
e5c8380ba7 feat(application): add FederationManagementEventService 2026-05-28 02:28:15 +02:00
97bc918bbc fix(bootstrap,worker): pass shared federation_repo to ApFederationAdapter 2026-05-28 02:26:57 +02:00
805240aaf8 feat(activitypub): add federation_repo field and thin DB-only methods to ApFederationAdapter 2026-05-28 02:24:43 +02:00
cd6148eff9 feat(domain): add mark_follower_accepted/rejected thin port methods 2026-05-28 02:22:52 +02:00
6f1a0572df fix(event-payload): correct NATS subjects for federation events 2026-05-28 02:20:43 +02:00
0841554dbe feat(domain): add RemoteFollowAccepted, RemoteFollowRejected, ActorMoved events 2026-05-28 02:19:46 +02:00
c30243f1c8 feat: add alsoKnownAs field to federation settings
Some checks failed
lint / lint (push) Failing after 7m10s
test / unit (push) Successful in 16m28s
test / integration (push) Failing after 17m52s
2026-05-28 02:01:56 +02:00
1ad02e0806 feat: add PATCH /federation/me/also-known-as endpoint
Some checks failed
test / unit (push) Has been cancelled
test / integration (push) Has been cancelled
lint / lint (push) Has been cancelled
Adds alsoKnownAs column to users table (migration 013), reads it in
the AP actor JSON, and exposes PATCH /federation/me/also-known-as to
set or clear it. Required pre-condition for broadcast_move.
2026-05-28 01:59:35 +02:00
23a8444b5c fix: update user URL handling in ThoughtsObjectHandler to use user_id
Some checks failed
lint / lint (push) Failing after 7m19s
test / integration (push) Has been cancelled
test / unit (push) Has been cancelled
2026-05-28 01:51:55 +02:00
b20b60ad10 fix: add broadcast_move stub to TestStore
Some checks failed
test / unit (push) Has been cancelled
test / integration (push) Has been cancelled
lint / lint (push) Has been cancelled
2026-05-28 01:50:37 +02:00
54bd2b60d0 feat: add POST /federation/me/move endpoint 2026-05-28 01:47:29 +02:00
94193f2d2e feat: bump k-ap to v0.1.9 and implement migrate_follower_actor 2026-05-28 01:43:06 +02:00
f54fb543b2 feat: update k-ap dependency to v0.1.8 and enhance middleware for ActivityPub requests 2026-05-28 01:08:45 +02:00
a460428be1 feat: update dependencies to k-ap v0.1.7 and add profileHref utility for user links
Some checks failed
lint / lint (push) Failing after 7m11s
test / unit (push) Successful in 16m59s
test / integration (push) Failing after 18m3s
2026-05-27 23:38:14 +02:00
95dea06c55 feat: add /about/fediverse info page with glass accordion panels 2026-05-27 23:38:14 +02:00
c085067318 feat: add Fediverse nav link 2026-05-27 23:38:14 +02:00
d831784489 feat: add copy handle button and fediverse info link to profile 2026-05-27 23:38:14 +02:00
4c203bed1d fix: handle clipboard errors and cleanup timeout in CopyButton 2026-05-27 23:38:14 +02:00
21b8684608 feat: add CopyButton client component 2026-05-27 23:38:14 +02:00
74eeb9fcb9 docs: replace activitypub-base with k-ap in architecture overview
Some checks failed
lint / lint (push) Failing after 7m3s
test / unit (push) Successful in 16m26s
test / integration (push) Failing after 17m39s
Reflects the migration from the local activitypub-base crate to the
external k-ap library, with an accurate description of what it provides.
2026-05-25 00:57:29 +02:00
7ee22ae79f feat: store AP note extensions in JSONB and render movies-diary posts as rich cards
Some checks failed
lint / lint (push) Failing after 7m24s
test / unit (push) Successful in 17m17s
test / integration (push) Failing after 18m2s
2026-05-24 04:29:04 +02:00
3f26456d77 feat: custom CSS editor with CodeMirror, live preview, and /docs/css reference
Some checks failed
lint / lint (push) Failing after 7m36s
test / unit (push) Successful in 17m12s
test / integration (push) Failing after 18m34s
2026-05-24 03:26:34 +02:00
379f31e27d fix(federation): include header_url as AP banner (image) in actor JSON
Some checks failed
lint / lint (push) Failing after 7m3s
test / unit (push) Successful in 16m12s
test / integration (push) Failing after 17m24s
2026-05-24 02:18:41 +02:00
9c99f7a7a8 feat: add image upload for avatar and banner
Some checks failed
lint / lint (push) Has been cancelled
test / unit (push) Has been cancelled
test / integration (push) Has been cancelled
2026-05-24 02:06:47 +02:00
636d3d453d fix: resolve thoughts compile errors after k-ap migration
Some checks failed
lint / lint (push) Failing after 5m0s
test / unit (push) Failing after 4m59s
test / integration (push) Failing after 5m2s
2026-05-17 23:02:49 +02:00
9172c82d54 chore: move ap_ports into activitypub adapter, delete activitypub-base 2026-05-17 22:48:22 +02:00
cd2eb48ddb chore: switch activitypub-base to k-ap git dep 2026-05-17 22:47:32 +02:00
c5d9833c8b refactor: replace long arg lists with input/config structs and builder
Some checks failed
lint / lint (push) Failing after 7m8s
test / unit (push) Successful in 17m2s
test / integration (push) Failing after 17m47s
- Thought::new_local → NewThought struct (7 args → 1)
- UserWriter::update_profile → UpdateProfileInput struct (6 args → 2)
- update_profile use case → UpdateProfileInput (8 args → 3)
- ActivityPubService::new → builder pattern (9 args → 5 required + 4 optional setters)
- accept_note → AcceptNoteInput struct (8 args → 1)
- ThoughtNote::new_public → ThoughtNoteInput struct (8 args → 1)

Remove all #[allow(clippy::too_many_arguments)] annotations.
2026-05-17 12:25:53 +02:00
f39c1a614d clean up
Some checks failed
lint / lint (push) Failing after 7m18s
test / integration (push) Has been cancelled
test / unit (push) Has been cancelled
2026-05-17 12:15:27 +02:00
30c8a17168 clean up
Some checks failed
test / unit (push) Has been cancelled
lint / lint (push) Has been cancelled
test / integration (push) Has been cancelled
2026-05-17 12:14:45 +02:00
6a8c8b1fb8 chore: add pre-commit fmt+clippy hooks, fix clippy warnings 2026-05-17 12:09:24 +02:00
4ec0725ff8 fmt
Some checks failed
lint / lint (push) Failing after 5m3s
test / integration (push) Has been cancelled
test / unit (push) Has been cancelled
2026-05-17 12:04:51 +02:00
31e0f2958c fix: make ThoughtNote sensitive field optional (default false) 2026-05-17 12:02:58 +02:00
555121ea75 fix: promote worker event logs from debug to info 2026-05-17 12:02:13 +02:00
9e795eefdc fix: make ThoughtNote url field optional for AP compat
Some checks failed
lint / lint (push) Failing after 5m1s
test / integration (push) Has been cancelled
test / unit (push) Has been cancelled
2026-05-17 11:57:10 +02:00
18cf2c9f54 feat: implement verify() for all stub activity handlers
Undo: inner activity actor must match Undo actor
Announce/Like/Block: verify_domains_match(activity_id, actor_url)
Add: attributedTo must match actor (same as Create/Update)
2026-05-17 11:55:17 +02:00
b58c96b843 feat: implement federation post/connections backfill schedulers
Some checks failed
lint / lint (push) Failing after 5m12s
test / integration (push) Has been cancelled
test / unit (push) Has been cancelled
schedule_actor_posts_fetch now spawns backfill_outbox in background,
fetching all pages of a remote outbox and persisting via accept_note.
schedule_connections_fetch follows AP collection next-links, resolves
profiles, and caches them in the DB. Both were no-ops ("deferred").

Add connections_repo field to ActivityPubService; wire both factories.
2026-05-17 11:49:53 +02:00
8ea24461ba feat: load more pagination for user profile thoughts 2026-05-16 15:21:18 +02:00
e14a9f90c8 fix: route local users to /users/{username} in remote connection lists 2026-05-16 15:17:58 +02:00
28756ef4cd feat: load more pagination for remote user posts 2026-05-16 15:14:53 +02:00
7f27ae49c3 fix: overflow-y scroll on html to prevent layout shift on dropdown open 2026-05-16 15:12:41 +02:00
59f3423c00 fix: break-all on fediverse handle to prevent overflow 2026-05-16 15:07:30 +02:00
c48aa33592 fix: scrollbar-gutter stable to prevent bg flicker on dropdown open 2026-05-16 15:05:28 +02:00
8f3aa4b891 fix: wrap background image in fixed div so it stays put on scroll 2026-05-16 15:03:41 +02:00
32bfb00970 feat: Frutiger Aero redesign — glass panels, Aero shimmer, interaction moments
Some checks failed
lint / lint (push) Failing after 5m7s
test / unit (push) Successful in 16m24s
test / integration (push) Failing after 18m14s
2026-05-16 14:55:51 +02:00
7ce2901c2a docs: add Frutiger Aero redesign implementation plan 2026-05-16 13:53:44 +02:00
8bbc713093 docs: add Frutiger Aero redesign spec 2026-05-16 13:46:25 +02:00
13 changed files with 9 additions and 388 deletions

View File

@@ -187,59 +187,6 @@ impl FollowRepository for PgFollowRepository {
.into_domain()?;
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)]

View File

@@ -56,86 +56,3 @@ async fn get_accepted_following_ids(pool: sqlx::PgPool) {
let ids = repo.get_accepted_following_ids(&alice.id).await.unwrap();
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());
}

View File

@@ -96,20 +96,6 @@ pub async fn list_remote_following(
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(
follows: &dyn FollowRepository,
users: &dyn UserReader,

View File

@@ -1,25 +1,6 @@
use super::*;
use chrono::Utc;
use domain::models::remote_actor::RemoteActor;
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]
async fn list_pending_returns_empty_by_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();
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());
}

View File

@@ -2,11 +2,7 @@ use chrono::Utc;
use domain::{
errors::DomainError,
events::DomainEvent,
models::{
feed::{PageParams, Paginated},
social::{Block, Boost, Follow, FollowState, Like},
user::User,
},
models::social::{Block, Boost, Follow, FollowState, Like},
ports::{
BlockRepository, BoostRepository, EventPublisher, FederationFollowPort, FollowRepository,
LikeRepository, UserReader,
@@ -284,13 +280,5 @@ pub async fn unblock_user(
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)]
mod tests;

View File

@@ -204,51 +204,3 @@ async fn boost_and_unboost() {
.iter()
.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);
}

View File

@@ -171,11 +171,6 @@ pub trait FollowRepository: Send + Sync {
&self,
user_id: &UserId,
) -> Result<Vec<UserId>, DomainError>;
async fn list_mutual(
&self,
user_id: &UserId,
page: &PageParams,
) -> Result<Paginated<User>, DomainError>;
}
#[async_trait]

View File

@@ -37,8 +37,6 @@ pub struct TestStore {
pub actor_ap_ids: Arc<Mutex<HashMap<String, UserId>>>,
/// ThoughtId → AP object URL (used by get_thought_ap_id)
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]
@@ -454,46 +452,6 @@ impl FollowRepository for TestStore {
.map(|f| f.following_id.clone())
.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]
@@ -752,7 +710,7 @@ impl FederationFollowPort for TestStore {
&self,
_user_id: &UserId,
) -> Result<Vec<RemoteActor>, DomainError> {
Ok(self.remote_following.lock().unwrap().clone())
Ok(vec![])
}
async fn broadcast_move(
@@ -793,7 +751,7 @@ impl FederationFollowRequestPort for TestStore {
&self,
_user_id: &UserId,
) -> Result<Vec<RemoteActor>, DomainError> {
Ok(self.remote_followers.lock().unwrap().clone())
Ok(vec![])
}
async fn remove_remote_follower(

View File

@@ -5,8 +5,8 @@ use crate::{
};
use api_types::responses::{ProfileField, RemoteActorResponse};
use application::use_cases::federation_management::{
accept_follow_request, get_remote_friends, initiate_actor_move, list_pending_requests,
list_remote_followers, list_remote_following, reject_follow_request, remove_remote_following,
accept_follow_request, initiate_actor_move, list_pending_requests, list_remote_followers,
list_remote_following, reject_follow_request, remove_remote_following,
};
use axum::{http::StatusCode, Json};
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()))
}
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(
Deps(d): Deps<FederationManagementDeps>,
AuthUser(uid): AuthUser,

View File

@@ -4,15 +4,11 @@ use crate::{
errors::ApiError,
extractors::{AuthUser, Deps},
};
use api_types::requests::{PaginationQuery, SetTopFriendsRequest};
use api_types::responses::{PagedResponse, TopFriendsResponse, UserResponse};
use api_types::requests::SetTopFriendsRequest;
use api_types::responses::TopFriendsResponse;
use application::use_cases::profile::{get_top_friends, get_user_by_username, set_top_friends};
use application::use_cases::social::*;
use axum::{
extract::{Path, Query},
http::StatusCode,
Json,
};
use axum::{extract::Path, http::StatusCode, Json};
use domain::{
ports::{
BlockRepository, BoostRepository, EventPublisher, FederationActionPort, FollowRepository,
@@ -154,33 +150,5 @@ pub async fn get_top_friends_handler(
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)]
mod tests;

View File

@@ -1,10 +1,9 @@
use super::get_friends_handler;
use super::*;
use crate::testing::make_state;
use axum::{
body::Body,
http::Request,
routing::{delete, get, post},
routing::{delete, post},
Router,
};
use tower::ServiceExt;
@@ -33,24 +32,6 @@ async fn follow_without_auth_returns_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]
async fn unfollow_remote_without_auth_returns_401() {
let resp = app()

View File

@@ -14,7 +14,6 @@ use utoipa::OpenApi;
crate::handlers::social::delete_block,
crate::handlers::social::put_top_friends,
crate::handlers::social::get_top_friends_handler,
crate::handlers::social::get_friends_handler,
),
components(schemas(SetTopFriendsRequest))
)]

View File

@@ -26,7 +26,6 @@ pub fn router() -> Router<AppState> {
put(users::upload_banner).layer(DefaultBodyLimit::max(10 * 1024 * 1024)),
)
.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/{username}", get(users::get_user))
.route(
@@ -105,10 +104,6 @@ pub fn router() -> Router<AppState> {
get(federation_management::get_remote_following)
.delete(federation_management::delete_following),
)
.route(
"/federation/me/friends",
get(federation_management::get_remote_friends_handler),
)
.route(
"/federation/me/move",
post(federation_management::post_move_account),