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}