this repo has no description

add suggested users feed

+16 -10
cmd/peruse/main.go
···
EnvVars: []string{"PERUSE_CHRONO_FEED_RKEY"},
Required: true,
},
+
&cli.StringFlag{
+
Name: "suggested-follows-rkey",
+
EnvVars: []string{"PERUSE_SUGGESTED_FOLLOWS_RKEY"},
+
Required: true,
+
},
},
Action: run,
}
···
}))
server, err := peruse.NewServer(peruse.ServerArgs{
-
HttpAddr: cmd.String("http-addr"),
-
ClickhouseAddr: cmd.String("clickhouse-addr"),
-
ClickhouseDatabase: cmd.String("clickhouse-database"),
-
ClickhouseUser: cmd.String("clickhouse-user"),
-
ClickhousePass: cmd.String("clickhouse-pass"),
-
Logger: logger,
-
FeedOwnerDid: cmd.String("feed-owner-did"),
-
ServiceDid: cmd.String("service-did"),
-
ServiceEndpoint: cmd.String("service-endpoint"),
-
ChronoFeedRkey: cmd.String("chrono-feed-rkey"),
+
HttpAddr: cmd.String("http-addr"),
+
ClickhouseAddr: cmd.String("clickhouse-addr"),
+
ClickhouseDatabase: cmd.String("clickhouse-database"),
+
ClickhouseUser: cmd.String("clickhouse-user"),
+
ClickhousePass: cmd.String("clickhouse-pass"),
+
Logger: logger,
+
FeedOwnerDid: cmd.String("feed-owner-did"),
+
ServiceDid: cmd.String("service-did"),
+
ServiceEndpoint: cmd.String("service-endpoint"),
+
ChronoFeedRkey: cmd.String("chrono-feed-rkey"),
+
SuggestedFollowsRkey: cmd.String("suggested-follows-rkey"),
})
if err != nil {
logger.Error("error creating server", "error", err)
+77
peruse/get_suggested_follows.go
···
+
package peruse
+
+
import (
+
"context"
+
"time"
+
)
+
+
type SuggestedFollow struct {
+
SuggestedDid string `ch:"suggested_did"`
+
BskyUrl string `ch:"bsky_url"`
+
FollowedByCount uint64 `ch:"followed_by_count"`
+
}
+
+
func (u *User) getSuggestedFollows(ctx context.Context, s *Server) ([]SuggestedFollow, error) {
+
if !time.Now().After(u.suggestedFollowsExpiresAt) {
+
return u.suggestedFollows, nil
+
}
+
+
u.mu.Lock()
+
defer u.mu.Unlock()
+
+
if !time.Now().After(u.suggestedFollowsExpiresAt) {
+
return u.suggestedFollows, nil
+
}
+
+
var suggestedFollows []SuggestedFollow
+
if err := s.conn.Select(ctx, &suggestedFollows, getSuggestedFollowsQuery, u.did); err != nil {
+
return nil, err
+
}
+
+
u.suggestedFollowsExpiresAt = time.Now().Add(1 * time.Hour)
+
+
return suggestedFollows, nil
+
}
+
+
const getSuggestedFollowsQuery = `
+
WITH ? as your_did,
+
now() - interval 60 day AS timeframe,
+
40 as top_mutual_limit,
+
20 as second_level_limit
+
+
SELECT
+
f.subject as suggested_did,
+
concat('https://bsky.app/profile/', f.subject) as bsky_url,
+
COUNT(*) as followed_by_count
+
FROM follow f
+
WHERE
+
f.subject != your_did
+
AND f.subject NOT IN (SELECT subject FROM follow WHERE did = your_did)
+
AND f.did IN (
+
SELECT i.subject_did
+
FROM interaction i
+
WHERE
+
i.kind = 'like'
+
AND i.created_at > timeframe
+
AND i.did IN (
+
SELECT i1.subject_did
+
FROM interaction i1, interaction_reverse i2
+
WHERE i1.subject_did = i2.did
+
AND i2.subject_did = your_did
+
AND i2.kind = 'like'
+
AND i1.did = your_did
+
AND i1.kind = 'like'
+
AND i1.created_at > timeframe
+
GROUP BY i1.subject_did
+
ORDER BY COUNT(*) DESC
+
LIMIT top_mutual_limit
+
)
+
GROUP BY i.subject_did
+
ORDER BY COUNT(*) DESC
+
LIMIT second_level_limit
+
)
+
GROUP BY f.subject
+
HAVING COUNT(*) >= 2
+
ORDER BY followed_by_count DESC
+
LIMIT 100
+
`
+8 -27
peruse/handle_chrono_feed.go
···
package peruse
import (
-
"fmt"
-
"strings"
-
"github.com/haileyok/peruse/internal/helpers"
-
"github.com/haileyok/photocopy/models"
"github.com/labstack/echo/v4"
+
)
+
+
const (
+
DefaultCursor = "9999999999999"
)
func (s *Server) handleChronoFeed(e echo.Context, req FeedSkeletonRequest) error {
···
cbdids = cbdids[1:] // remove self
if req.Cursor == "" {
-
req.Cursor = "9999999999999" // hack for simplicity...
+
req.Cursor = DefaultCursor // hack for simplicity...
}
-
var posts []models.Post
-
if err := s.conn.Select(ctx, &posts, fmt.Sprintf(`
-
SELECT uri
-
FROM default.post
-
WHERE rkey < ?
-
AND did IN (?)
-
AND parent_uri = ''
-
ORDER BY created_at DESC
-
LIMIT 50
-
`), req.Cursor, cbdids); err != nil {
+
posts, err := s.getPostsForDidsChronological(ctx, cbdids, req.Cursor)
+
if err != nil {
s.logger.Error("error getting close by chrono posts", "error", err)
return helpers.ServerError(e, "FeedError", "")
}
···
return helpers.ServerError(e, "FeedError", "Not enough posts")
}
-
var fpis []FeedPostItem
-
var cursor string
-
-
for i, p := range posts {
-
fpis = append(fpis, FeedPostItem{
-
Post: p.Uri,
-
})
-
if i == len(posts)-1 {
-
pts := strings.Split(p.Uri, "/")
-
cursor = pts[len(pts)-1]
-
}
-
}
+
fpis, cursor := modelPostsToFeedItems(posts)
return e.JSON(200, FeedSkeletonResponse{
Cursor: &cursor,
+2
peruse/handle_feed_skeleton.go
···
switch aturi.RecordKey().String() {
case s.args.ChronoFeedRkey:
return s.handleChronoFeed(e, req)
+
case s.args.SuggestedFollowsRkey:
+
return s.handleSuggestedFollowsFeed(e, req)
default:
s.logger.Warn("invalid feed requested", "requested-feed", req.Feed)
return helpers.InputError(e, "FeedNotFound", "")
+43
peruse/handle_suggested_follows_feed.go
···
+
package peruse
+
+
import (
+
"github.com/haileyok/peruse/internal/helpers"
+
"github.com/labstack/echo/v4"
+
)
+
+
func (s *Server) handleSuggestedFollowsFeed(e echo.Context, req FeedSkeletonRequest) error {
+
ctx := e.Request().Context()
+
u := e.Get("user").(*User)
+
+
suggFollows, err := u.getCloseBy(ctx, s)
+
if err != nil {
+
s.logger.Error("error getting suggested follows for user", "user", u.did, "error", err)
+
return helpers.ServerError(e, "FeedError", "")
+
}
+
+
suggDids := []string{}
+
for _, sugg := range suggFollows {
+
suggDids = append(suggDids, sugg.SuggestedDid)
+
}
+
+
if req.Cursor == "" {
+
req.Cursor = DefaultCursor
+
}
+
+
posts, err := s.getPostsForDidsChronological(ctx, suggDids, req.Cursor)
+
if err != nil {
+
s.logger.Error("error getting suggested follows chrono posts", "error", err)
+
return helpers.ServerError(e, "FeedError", "")
+
}
+
+
if len(posts) == 0 {
+
return helpers.ServerError(e, "FeedError", "Not enough posts")
+
}
+
+
fpis, cursor := modelPostsToFeedItems(posts)
+
+
return e.JSON(200, FeedSkeletonResponse{
+
Cursor: &cursor,
+
Feed: fpis,
+
})
+
}
+11 -10
peruse/peruse.go
···
}
type ServerArgs struct {
-
Logger *slog.Logger
-
HttpAddr string
-
ClickhouseAddr string
-
ClickhouseDatabase string
-
ClickhouseUser string
-
ClickhousePass string
-
FeedOwnerDid string
-
ServiceDid string
-
ServiceEndpoint string
-
ChronoFeedRkey string
+
Logger *slog.Logger
+
HttpAddr string
+
ClickhouseAddr string
+
ClickhouseDatabase string
+
ClickhouseUser string
+
ClickhousePass string
+
FeedOwnerDid string
+
ServiceDid string
+
ServiceEndpoint string
+
ChronoFeedRkey string
+
SuggestedFollowsRkey string
}
func NewServer(args ServerArgs) (*Server, error) {
+39
peruse/post.go
···
+
package peruse
+
+
import (
+
"context"
+
"strings"
+
+
"github.com/haileyok/photocopy/models"
+
)
+
+
func (s *Server) getPostsForDidsChronological(ctx context.Context, dids []string, cursor string) ([]models.Post, error) {
+
var posts []models.Post
+
if err := s.conn.Select(ctx, &posts, `
+
SELECT uri
+
FROM default.post
+
WHERE rkey < ?
+
AND did IN (?)
+
AND parent_uri = ''
+
ORDER BY created_at DESC
+
LIMIT 50
+
`, cursor, dids); err != nil {
+
return nil, err
+
}
+
return posts, nil
+
}
+
+
func modelPostsToFeedItems(posts []models.Post) ([]FeedPostItem, string) {
+
var fpis []FeedPostItem
+
var cursor string
+
for i, p := range posts {
+
fpis = append(fpis, FeedPostItem{
+
Post: p.Uri,
+
})
+
if i == len(posts)-1 {
+
pts := strings.Split(p.Uri, "/")
+
cursor = pts[len(pts)-1]
+
}
+
}
+
return fpis, cursor
+
}
+3
peruse/user.go
···
closeBy []CloseBy
closeByExpiresAt time.Time
+
+
suggestedFollows []SuggestedFollow
+
suggestedFollowsExpiresAt time.Time
}
func NewUser(did string) *User {