feeds.gno

4.34 Kb ยท 190 lines
  1package social_feed
  2
  3import (
  4	"strconv"
  5	"strings"
  6
  7	"gno.land/p/nt/avl"
  8	"gno.land/p/nt/seqid"
  9	feedsv1 "gno.land/p/zenao/feeds/v1"
 10	"gno.land/p/zenao/realmid"
 11)
 12
 13type Feed struct {
 14	CrossNetwork bool
 15	AuthFunc     AuthFn
 16}
 17
 18func (f *Feed) auth() string {
 19	if f.AuthFunc == nil {
 20		return realmid.Previous()
 21	}
 22
 23	id, ok := f.AuthFunc()
 24	if !ok {
 25		panic("the user is not allowed to interact with this feed")
 26	}
 27	return id
 28}
 29
 30type AuthFn func() (userID string, authorized bool)
 31
 32var (
 33	id seqid.ID
 34
 35	reactions     *avl.Tree // post_id:icon -> count
 36	userReactions *avl.Tree // post_id:icon:user_address -> struct{}
 37
 38	posts       *avl.Tree // local_post_id -> *Post // for search with specific id (get/delete)
 39	postsByFeed *avl.Tree // feed_id:local_post_id -> *Post // for search with feed id (iterate)
 40
 41	// XXX: add feed index into the composite key to ensure reply are in the same feed ?
 42	postsByParent *avl.Tree // parent_id:local_post_id -> *Post // for search with parent_id
 43	feeds         *avl.Tree // slug -> *Feed
 44
 45	feedsByPost *avl.Tree // post_id -> feed_id
 46)
 47
 48func init() {
 49	reactions = avl.NewTree()
 50	userReactions = avl.NewTree()
 51
 52	posts = avl.NewTree()
 53	postsByFeed = avl.NewTree()
 54	postsByParent = avl.NewTree()
 55	feeds = avl.NewTree()
 56	feedsByPost = avl.NewTree()
 57}
 58
 59func NewFeed(_ realm, slug string, crossNetwork bool, authFunc AuthFn) string {
 60	assertActive()
 61
 62	creator := realmid.Previous()
 63	if crossNetwork && authFunc != nil {
 64		panic("crossNetwork feeds cannot have an authFunc")
 65	}
 66
 67	feed := &Feed{
 68		CrossNetwork: crossNetwork,
 69		AuthFunc:     authFunc,
 70	}
 71	feedId := creator + ":" + slug
 72	if feeds.Has(feedId) {
 73		panic("feed already exists")
 74	}
 75	feeds.Set(feedId, feed)
 76	return feedId
 77}
 78
 79// XXX: do a registry by user ?
 80// XXX: handle flagged posts
 81// XXX: handle tags with an array of string
 82// XXX: optimize research by tags with a new tree ?
 83func GetFeedPosts(feedId string, offset uint32, limit uint32, tags string, user string) []*feedsv1.PostView {
 84	if feedId == "" {
 85		panic("feedId is empty")
 86	}
 87	var res []*feedsv1.PostView
 88	i := 0
 89	start := feedId + ":"
 90	end := feedId + ";"
 91	tagsSplit := []string{}
 92	if tags != "" {
 93		tagsSplit = strings.Split(tags, ",")
 94	}
 95	postsByFeed.ReverseIterate(start, end, func(key string, value interface{}) bool {
 96		post := value.(*feedsv1.Post)
 97		for _, tag := range tagsSplit {
 98			if !hasTag(post.Tags, tag) {
 99				return false
100			}
101		}
102		if post.DeletedAt != 0 {
103			return false
104		}
105		if post.ParentUri != "" {
106			return false
107		}
108		if i >= int(offset) {
109			postView := &feedsv1.PostView{
110				Post:          post,
111				ChildrenCount: CountChildren(post.LocalPostId),
112				Reactions:     GetPostReactions(post.LocalPostId, user),
113			}
114			res = append(res, postView)
115		}
116		i++
117		if i >= int(offset+limit) {
118			return true
119		}
120		return false
121	})
122	return res
123}
124
125// XXX: merge with above function (deduct from param the tree to use) ?
126func GetChildrenPosts(parentId string, offset uint32, limit uint32, tags string, user string) []*feedsv1.PostView {
127	if parentId == "" {
128		panic("parentId is empty")
129	}
130	parentIdInt, err := strconv.ParseUint(parentId, 10, 64)
131	if err != nil {
132		panic("parentId is not a valid uint64")
133	}
134	var res []*feedsv1.PostView
135	i := 0
136	start := seqid.ID(parentIdInt).String() + ":"
137	end := seqid.ID(parentIdInt).String() + ";"
138	tagsSplit := []string{}
139	if tags != "" {
140		tagsSplit = strings.Split(tags, ",")
141	}
142	postsByParent.ReverseIterate(start, end, func(key string, value interface{}) bool {
143		post := value.(*feedsv1.Post)
144		for _, tag := range tagsSplit {
145			if !hasTag(post.Tags, tag) {
146				return false
147			}
148		}
149		if post.DeletedAt != 0 {
150			return false
151		}
152		if i >= int(offset) {
153			postView := &feedsv1.PostView{
154				Post:          post,
155				ChildrenCount: CountChildren(post.LocalPostId),
156				Reactions:     GetPostReactions(post.LocalPostId, user),
157			}
158			res = append(res, postView)
159		}
160		i++
161		if i >= int(offset+limit) {
162			return true
163		}
164		return false
165	})
166	return res
167}
168
169func CountChildren(parentId uint64) uint64 {
170	count := uint64(0)
171	parentIdStr := seqid.ID(parentId).String()
172	postsByParent.ReverseIterate(parentIdStr+":", parentIdStr+";", func(key string, value interface{}) bool {
173		post := value.(*feedsv1.Post)
174		if post.DeletedAt != 0 {
175			return false
176		}
177		count++
178		return false
179	})
180	return count
181}
182
183func hasTag(tags []string, tag string) bool {
184	for _, t := range tags {
185		if t == tag {
186			return true
187		}
188	}
189	return false
190}