Skip to content Search Apps Documentation Source Content File Folder Download Copy Actions State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

Community realm. Read the code first. Community realm, deployed by its author. Read the code before you interact or send coins.

post.gno

4.64 Kb · 214 lines
  1package gnocial
  2
  3import (
  4	"chain"
  5	"chain/runtime"
  6	"math/overflow"
  7	"strconv"
  8)
  9
 10// Post publishes a top-level post and returns its id.
 11// gated is yes/1/true/khoa to withhold the body from Render.
 12// The body is still public chain state. Pass an empty gated value for a public post.
 13func Post(cur realm, body, gated string) int {
 14	who := caller(cur)
 15	mustNoPay()
 16	if state.paused {
 17		panic("paused")
 18	}
 19	author := mustMember(who)
 20	return writePost(author, 0, body, isYes(gated))
 21}
 22
 23// Reply publishes a reply to parent and returns the new id. Replies are public.
 24func Reply(cur realm, parent int, body string) int {
 25	who := caller(cur)
 26	mustNoPay()
 27	if state.paused {
 28		panic("paused")
 29	}
 30	author := mustMember(who)
 31	target := postByID(parent)
 32	if target == nil {
 33		panic("unknown post")
 34	}
 35	if target.Deleted {
 36		panic("deleted")
 37	}
 38	id := writePost(author, parent, body, false)
 39	target.Replies++
 40	state.replies.Set(pad(parent)+"/"+pad(id), id)
 41	return id
 42}
 43
 44func writePost(author *Profile, parent int, body string, gated bool) int {
 45	body = cleanBody(body)
 46	id := state.nextID
 47	state.nextID++
 48	p := &Article{
 49		ID:      id,
 50		Author:  author.Addr,
 51		Body:    body,
 52		Height:  runtime.ChainHeight(),
 53		ReplyTo: parent,
 54		Gated:   gated,
 55	}
 56	state.posts.Set(pad(id), p)
 57	if parent == 0 {
 58		author.Posts++
 59		state.feed.Set(pad(id), id)
 60		state.byAuthor.Set(authorKey(author.Addr, id), id)
 61		chain.Emit(
 62			"ArenaPost",
 63			"id", strconv.Itoa(id),
 64			"author", author.Addr.String(),
 65			"handle", author.Handle,
 66			"gated", yesNo(gated),
 67		)
 68	} else {
 69		chain.Emit(
 70			"ArenaReply",
 71			"id", strconv.Itoa(id),
 72			"parent", strconv.Itoa(parent),
 73			"author", author.Addr.String(),
 74			"handle", author.Handle,
 75		)
 76	}
 77	return id
 78}
 79
 80func yesNo(v bool) string {
 81	if v {
 82		return "yes"
 83	}
 84	return "no"
 85}
 86
 87// Delete soft-deletes the caller's post and clears its body from current state.
 88// The original transaction still holds the text in chain history.
 89func Delete(cur realm, id int) {
 90	who := caller(cur)
 91	mustNoPay()
 92	mustMember(who)
 93	p := postByID(id)
 94	if p == nil {
 95		panic("unknown post")
 96	}
 97	if p.Deleted {
 98		panic("deleted")
 99	}
100	if p.Author != who {
101		panic("author only")
102	}
103	if p.ReplyTo == 0 {
104		state.feed.Remove(pad(id))
105		state.byAuthor.Remove(authorKey(who, id))
106		if author := profileByAddr(who); author != nil && author.Posts > 0 {
107			author.Posts--
108		}
109	} else {
110		state.replies.Remove(pad(p.ReplyTo) + "/" + pad(id))
111		if parent := postByID(p.ReplyTo); parent != nil && parent.Replies > 0 {
112			parent.Replies--
113		}
114	}
115	p.Deleted = true
116	p.Body = ""
117	p.Gated = false
118	chain.Emit("ArenaDelete", "id", strconv.Itoa(id), "author", who.String())
119}
120
121// Like adds the caller's like. One like per account.
122func Like(cur realm, id int) {
123	who := caller(cur)
124	mustNoPay()
125	mustMember(who)
126	p := livePost(id)
127	k := likeKey(id, who)
128	if state.likes.Has(k) {
129		panic("already liked")
130	}
131	state.likes.Set(k, 1)
132	p.Likes++
133	chain.Emit("ArenaLike", "id", strconv.Itoa(id), "from", who.String())
134}
135
136// Unlike removes the caller's like.
137func Unlike(cur realm, id int) {
138	who := caller(cur)
139	mustNoPay()
140	mustMember(who)
141	p := livePost(id)
142	k := likeKey(id, who)
143	if !state.likes.Has(k) {
144		panic("not liked")
145	}
146	state.likes.Remove(k)
147	if p.Likes > 0 {
148		p.Likes--
149	}
150	chain.Emit("ArenaUnlike", "id", strconv.Itoa(id), "from", who.String())
151}
152
153// Tip forwards the attached ugnot to the post's author. The realm does not keep it.
154func Tip(cur realm, id int) {
155	who := caller(cur)
156	if state.paused {
157		panic("paused")
158	}
159	mustMember(who)
160	sent := takeUgnot(cur)
161	p := livePost(id)
162	author := profileByAddr(p.Author)
163	if author == nil {
164		panic("register first")
165	}
166	p.Tips = overflow.Add64p(p.Tips, sent)
167	author.TipsIn = overflow.Add64p(author.TipsIn, sent)
168	if self := profileByAddr(who); self != nil {
169		self.TipsOut = overflow.Add64p(self.TipsOut, sent)
170	}
171	payout(cur, p.Author, sent, "author")
172	chain.Emit(
173		"ArenaTip",
174		"id", strconv.Itoa(id),
175		"from", who.String(),
176		"to", p.Author.String(),
177		"ugnot", strconv.FormatInt(sent, 10),
178	)
179}
180
181// GatedBody returns a gated post's text for the author or a current ticket holder.
182// Chain storage is public; this only gates the convenience call and Render.
183func GatedBody(cur realm, id int) string {
184	who := caller(cur)
185	mustNoPay()
186	mustMember(who)
187	p := livePost(id)
188	if !p.Gated {
189		return p.Body
190	}
191	if p.Author != who && getShares(who, p.Author) < 1 {
192		panic("need a ticket")
193	}
194	return p.Body
195}
196
197func livePost(id int) *Article {
198	p := postByID(id)
199	if p == nil {
200		panic("unknown post")
201	}
202	if p.Deleted {
203		panic("deleted")
204	}
205	return p
206}
207
208// Liked reports whether who liked the post.
209func Liked(who address, id int) bool {
210	if id <= 0 {
211		return false
212	}
213	return state.likes.Has(likeKey(id, who))
214}