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

owner.gno

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