owner.gno
8.43 Kb · 241 lines
1package golf
2
3import (
4 "chain"
5 "chain/runtime"
6 "chain/runtime/unsafe"
7 "strconv"
8 "strings"
9)
10
11// The course's owner, the deployer, set at init: golf's only role. Every
12// check is on the immediate caller (cur.Previous().Address()), never the
13// signer, so a realm the owner calls cannot act in their name. The role
14// passes by Transfer then Accept; Renounce freezes the course for good.
15var (
16 owner address // "" once renounced
17 pending address // the address a Transfer offered the role to, "" if none
18)
19
20// self is golf's own pkgpath, hub its gnoweb path: every link follows from
21// them, so the same code serves under any namespace.
22var (
23 self = unsafe.CurrentRealm().PkgPath()
24 hub = strings.TrimPrefix(self, "gno.land")
25 officialPrefix = self[:strings.LastIndex(self, "/")+1] // the namespace's realms, as "…/"
26)
27
28// playURL is the 3D client every page links to (SetPlayURL moves it).
29var playURL = "https://gnogolf.xyz/"
30
31// successor is the realm that took this course over, "" until the owner
32// names it, once (SetSuccessor).
33var successor string
34
35func init() { setUp(unsafe.OriginCaller(), runtime.ChainID()) }
36
37// setUp is init: the deployer owns the course, and community publishing
38// starts closed. Only `gno test` (chain "dev") runs it with no signer: the
39// owner stays "", which holds no power, and publishing starts open, since
40// nobody there could open it.
41func setUp(deployer address, chainID string) {
42 if deployer == "" && chainID != "dev" {
43 panic("golf: no owner: golf must be deployed by an account")
44 }
45 owner, publishing = deployer, deployer == ""
46}
47
48// Owner is the course's owner, "" once renounced.
49func Owner() address { return owner }
50
51// Pending is the address a Transfer offered the role to, "" if none.
52func Pending() address { return pending }
53
54// Successor is the realm that took this course over, "" if none.
55func Successor() string { return successor }
56
57// isOwner is the one owner check: a live role, held by the caller.
58func isOwner(caller address) bool { return owner != "" && caller == owner }
59
60// Transfer offers the role to another address, which takes it with Accept.
61// Offering it to the owner themselves cancels an offer.
62func Transfer(cur realm, to address) {
63 if !isOwner(cur.Previous().Address()) {
64 panic("golf: only the owner can transfer the course")
65 }
66 if !to.IsValid() {
67 panic("golf: not an address: " + to.String())
68 }
69 if to == owner {
70 pending = ""
71 chain.Emit(EventOwnershipOfferCancelled, "owner", owner.String())
72 return
73 }
74 pending = to
75 chain.Emit(EventOwnershipOffered, "owner", owner.String(), "to", to.String())
76}
77
78// Accept takes the role a Transfer offered the caller.
79func Accept(cur realm) {
80 caller := cur.Previous().Address()
81 if owner == "" || pending == "" || caller != pending {
82 panic("golf: no transfer of the course is offered to you")
83 }
84 from := owner
85 owner, pending = caller, ""
86 chain.Emit(EventOwnershipTransfer, "from", from.String(), "to", owner.String())
87}
88
89// Renounce gives the role up for good: no owner, no offer, and none possible.
90func Renounce(cur realm) {
91 if !isOwner(cur.Previous().Address()) {
92 panic("golf: only the owner can renounce the course")
93 }
94 from := owner
95 owner, pending = "", ""
96 chain.Emit(EventOwnershipRenounced, "from", from.String())
97}
98
99// maxURL bounds the play link, maxPkgPath a successor's pkgpath.
100const (
101 maxURL = 100
102 maxPkgPath = 100
103)
104
105// SetPlayURL moves the link every page gives to the 3D client. Only the
106// owner can. url is https://, at most 100 of letters, digits and "-._~/:%",
107// with no query or fragment: a hole's link adds "?cup=…&hole=…" to it.
108func SetPlayURL(cur realm, url string) {
109 if !isOwner(cur.Previous().Address()) {
110 panic("golf: only the owner can move the play link")
111 }
112 if !validURL(url) {
113 panic("golf: a play link is https:// and up to 100 of a-z, A-Z, 0-9 and -._~/:%")
114 }
115 playURL = url
116 chain.Emit(EventPlayURLSet, "url", url)
117}
118
119func validURL(u string) bool {
120 if len(u) > maxURL || !strings.HasPrefix(u, "https://") || len(u) == len("https://") {
121 return false
122 }
123 for i := 0; i < len(u); i++ {
124 c := u[i]
125 if !(c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z' || c >= '0' && c <= '9' || strings.IndexByte("-._~/:%", c) >= 0) {
126 return false
127 }
128 }
129 return true
130}
131
132// SetSuccessor names the realm that takes this course over, once and for
133// good: a pkgpath under gno.land/r/, not golf itself, with no "", "." or ".."
134// segment. Only the owner can. It moves and blocks nothing: the pages and
135// Holes say where the course went, and play goes on here. golf does not check
136// the realm exists: check the path before sending.
137func SetSuccessor(cur realm, pkgpath string) {
138 if !isOwner(cur.Previous().Address()) {
139 panic("golf: only the owner can name a successor")
140 }
141 if successor != "" {
142 panic("golf: the successor is already " + successor)
143 }
144 if !validPkgPath(pkgpath) || pkgpath == self {
145 panic("golf: a successor is a realm's pkgpath, gno.land/r/ and up to 100 of a-z, 0-9 and _-/.")
146 }
147 successor = pkgpath
148 chain.Emit(EventSuccessorSet, "successor", pkgpath)
149}
150
151// Hide takes a community hole ("<address>/<slug>", every version) off the
152// lists (the hub, Holes, the 3D game), for a scam or abuse in its name or
153// note; hide=false puts it back. Only the owner can. Its data, rounds and
154// page stay, Community still has it, and a new version stays hidden. A hole
155// already as asked is left alone, with no event.
156func Hide(cur realm, slot string, hide bool) {
157 if !isOwner(cur.Previous().Address()) {
158 panic("golf: only the owner can hide a hole")
159 }
160 v := aliases.Get(slot)
161 if v == nil {
162 if e := find(slot); e != nil && !e.official {
163 panic("golf: " + slot + " is a version: hide its hole, " + e.slot)
164 }
165 panic("golf: " + slot + " is not a community hole")
166 }
167 if hidden.Has(slot) == hide {
168 return
169 }
170 // out of byAuthor, all the lists walk: they never read a hidden hole
171 e := find(v.(string))
172 a := e.by.String()
173 row := a + " " + recentKey(e.height, slot[len(a)+1:])
174 was := newestRow(a)
175 if hide {
176 hidden.Set(slot, true)
177 byAuthor.Remove(row)
178 } else {
179 hidden.Remove(slot)
180 byAuthor.Set(row, e.id)
181 }
182 moveAuthor(a, was, newestRow(a))
183 chain.Emit(EventHoleHidden, "slot", slot, "hidden", strconv.FormatBool(hide))
184}
185
186// publishing is whether anyone can publish a community hole (PublishMine):
187// closed at deploy (setUp), opened later without a redeploy.
188var publishing bool
189
190// Publishing is whether PublishMine is open.
191func Publishing() bool { return publishing }
192
193// SetPublishing opens community publishing (PublishMine) or closes it again.
194// Only the owner can. Closing stops new holes and versions only: every hole
195// published stays playable and listed. Setting it as it is does nothing.
196func SetPublishing(cur realm, open bool) {
197 if !isOwner(cur.Previous().Address()) {
198 panic("golf: only the owner can open or close publishing")
199 }
200 if publishing == open {
201 return
202 }
203 publishing = open
204 chain.Emit(EventPublishingSet, "open", strconv.FormatBool(open))
205}
206
207func validPkgPath(p string) bool {
208 if len(p) > maxPkgPath || !strings.HasPrefix(p, "gno.land/r/") {
209 return false
210 }
211 for _, seg := range strings.Split(p[len("gno.land/r/"):], "/") {
212 if seg == "" || seg == "." || seg == ".." {
213 return false
214 }
215 }
216 for i := 0; i < len(p); i++ {
217 c := p[i]
218 if !(c >= 'a' && c <= 'z' || c >= '0' && c <= '9' || strings.IndexByte("_-/.", c) >= 0) {
219 return false
220 }
221 }
222 return true
223}
224
225// The events golf emits, and their attributes. A hole is always "hole", a
226// player "player".
227const (
228 EventHolePublished = "HolePublished" // hole, slot, sha, by, official ("true"/"false")
229 EventHoleRetired = "HoleRetired" // hole, next
230 EventShot = "Shot" // hole, player, strokes, mode, shot ("angle,power,tick" as recorded)
231 EventHoled = "Holed" // hole, player, strokes, mode, shots (the round's)
232 EventRoundReset = "RoundReset" // hole, player
233 EventOwnershipOffered = "OwnershipOffered" // owner, to
234 EventOwnershipOfferCancelled = "OwnershipOfferCancelled" // owner
235 EventOwnershipTransfer = "OwnershipTransfer" // from, to
236 EventOwnershipRenounced = "OwnershipRenounced" // from
237 EventPlayURLSet = "PlayURLSet" // url
238 EventSuccessorSet = "SuccessorSet" // successor
239 EventHoleHidden = "HoleHidden" // slot, hidden ("true"/"false")
240 EventPublishingSet = "PublishingSet" // open ("true"/"false")
241)