clone

a self-hosted multitenant nostr relay with sites, file storage, git hosting, and inbox and outbox delivery

feat/ui-updates
tinyrelay / internal / daemon / room_threads.go
room_threads.go 2.8K
1package daemon23import (4 "context"5 "errors"67 "github.com/FelineStateMachine/tinyrelay/internal/event"8 "github.com/FelineStateMachine/tinyrelay/internal/storage"9)1011var (12 errRoomThreadSizeLimit = errors.New("restricted: thread exceeds the browser limit")13 errRoomThreadDepthLimit = errors.New("restricted: thread exceeds the nesting limit")14)1516func (t *Tenant) roomThreadRoot(ctx context.Context, actor, room, id string) (event.Event, error) {17 seen := map[string]bool{}18 for len(seen) < 64 && !seen[id] {19 seen[id] = true20 rows, err := t.Query(ctx, []event.Filter{{IDs: []string{id}, Limit: intPtr(1)}}, browseSession(t, actor))21 if err != nil {22 return event.Event{}, err23 }24 if len(rows) != 1 || event.Tag(rows[0], "h") != room {25 break26 }27 root := rows[0]28 if root.Kind != 9 && root.Kind != 11 && root.Kind != 12 && root.Kind != 40002 {29 break30 }31 parent := event.RoomReplyRoot(root)32 if parent == "" {33 return root, nil34 }35 id = parent36 }37 return event.Event{}, errors.New("not found: thread")38}3940// Follow indexed references so parent-only Buzz replies join the same thread41// as marked root/reply pairs. The traversal is bounded against hostile trees.42func (t *Tenant) roomThreadReplies(ctx context.Context, actor, room, root string) ([]event.Event, error) {43 known := map[string]bool{root: true}44 frontier := []string{root}45 var replies []event.Event46 for depth := 0; len(frontier) > 0 && depth < 64; depth++ {47 var nextLevel []string48 for start := 0; start < len(frontier); start += 100 {49 end := min(start+100, len(frontier))50 filter := event.Filter{Kinds: []int{9, 12}, Tags: map[string][]string{"h": {room}, "e": frontier[start:end]}}51 var cursor *storage.EventCursor52 for {53 rows, next, err := t.roomMessages(ctx, actor, filter, cursor, 100)54 if err != nil {55 return nil, err56 }57 for _, row := range rows {58 if known[row.ID] || !known[event.RoomReplyRoot(row)] {59 continue60 }61 known[row.ID] = true62 replies = append(replies, row)63 nextLevel = append(nextLevel, row.ID)64 if len(replies) > 5000 {65 return nil, errRoomThreadSizeLimit66 }67 }68 if next == "" {69 break70 }71 cursor, err = parseCollaborationCursor(next)72 if err != nil {73 return nil, err74 }75 }76 }77 frontier = nextLevel78 }79 if len(frontier) > 0 {80 return nil, errRoomThreadDepthLimit81 }82 sortEventsNewestFirst(replies)83 return replies, nil84}8586func roomThreadPage(rows []event.Event, cursor *storage.EventCursor, limit int) ([]event.Event, string) {87 page := make([]event.Event, 0, limit)88 for _, row := range rows {89 if cursor != nil && (row.CreatedAt > cursor.CreatedAt || row.CreatedAt == cursor.CreatedAt && row.ID <= cursor.ID) {90 continue91 }92 if len(page) == limit {93 return page, collaborationCursor(collaborationItemFrom(page[len(page)-1]))94 }95 page = append(page, row)96 }97 return page, ""98}