Noreposts Feed
1use anyhow::Result;
2use std::sync::Arc;
3use tracing::warn;
4
5use crate::{
6 database::Database,
7 types::{FeedSkeletonResponse, SkeletonFeedPost},
8};
9
10pub struct FollowingNoRepostsFeed {
11 db: Arc<Database>,
12}
13
14impl FollowingNoRepostsFeed {
15 pub fn new(db: Arc<Database>) -> Self {
16 Self { db }
17 }
18
19 pub async fn generate_feed(
20 &self,
21 requester_did: Option<String>,
22 limit: Option<i32>,
23 cursor: Option<String>,
24 ) -> Result<FeedSkeletonResponse> {
25 // Require authentication for this feed since it's personalized
26 let follower_did = match requester_did {
27 Some(did) => did,
28 None => {
29 warn!("Unauthenticated request to following feed");
30 return Ok(FeedSkeletonResponse {
31 cursor: None,
32 feed: vec![],
33 });
34 }
35 };
36
37 let limit = limit.unwrap_or(50).min(100); // Cap at 100 items
38
39 // Get posts from accounts the user follows
40 let posts = self
41 .db
42 .get_following_posts(&follower_did, limit, cursor.as_deref())
43 .await?;
44
45 tracing::info!(
46 "Feed generated for {}: found {} posts from followed accounts",
47 follower_did,
48 posts.len()
49 );
50
51 let feed_posts: Vec<SkeletonFeedPost> = posts
52 .iter()
53 .map(|post| SkeletonFeedPost {
54 post: post.uri.clone(),
55 })
56 .collect();
57
58 // Generate cursor for pagination (use created_at for chronological order)
59 let cursor = posts.last().map(|post| post.created_at.to_rfc3339());
60
61 Ok(FeedSkeletonResponse {
62 cursor,
63 feed: feed_posts,
64 })
65 }
66}
67
68#[cfg(test)]
69mod tests {
70 use super::*;
71 use crate::types::{Follow, Post};
72 use chrono::Utc;
73
74 #[tokio::test]
75 async fn test_feed_generation() -> Result<()> {
76 let db = Arc::new(Database::new(":memory:").await?);
77 db.migrate().await?;
78
79 // Create test data
80 let follower_did = "did:example:alice";
81 let target_did = "did:example:bob";
82
83 // Insert follow relationship
84 let follow = Follow {
85 uri: format!("at://{}/app.bsky.graph.follow/test", follower_did),
86 follower_did: follower_did.to_string(),
87 target_did: target_did.to_string(),
88 created_at: Utc::now(),
89 indexed_at: Utc::now(),
90 };
91 db.insert_follow(&follow).await?;
92
93 // Insert post from followed user
94 let post = Post {
95 uri: format!("at://{}/app.bsky.feed.post/test", target_did),
96 cid: "test-cid".to_string(),
97 author_did: target_did.to_string(),
98 text: "Hello world!".to_string(),
99 created_at: Utc::now(),
100 indexed_at: Utc::now(),
101 };
102 db.insert_post(&post).await?;
103
104 let feed_algorithm = FollowingNoRepostsFeed::new(Arc::clone(&db));
105 let response = feed_algorithm
106 .generate_feed(Some(follower_did.to_string()), Some(10), None)
107 .await?;
108
109 assert_eq!(response.feed.len(), 1);
110 assert_eq!(response.feed[0].post, post.uri);
111
112 Ok(())
113 }
114}