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

catalog.gno

7.77 Kb · 222 lines
  1// Package catalog is GnoRadio's catalog rules, release 1: artists, tracks,
  2// albums, playlists, likes, follows, tips, support, moderation and
  3// verification. Other GnoRadio realms (radio, tickets, home) read it through
  4// the value getters in read.gno; it imports none of them.
  5//
  6// It keeps no state but its admin role. Every record lives in the data realm
  7// (gno.land/r/nym-alexiscolin000/gnoradio/data), in the "catalog/…" collections only the current
  8// catalog writer may change (db.gno). Promo budgets sit in data's vault, so a
  9// later release takes over the data and the coins without copying anything.
 10//
 11// Four track origins share one catalog:
 12//   - "artist":  published by the artist's own wallet, tips enabled;
 13//   - "curated": imported by the admin from CC0 / CC BY / CC BY-SA / CC BY-NC / CC BY-NC-SA sources;
 14//   - "audius", "jamendo": pointers the admin imports. The record keeps only
 15//     what the radio needs (platform id, genre, duration, licence); the app
 16//     reads title, artist and cover live, since both platforms' API terms
 17//     allow session caching only (attribution required).
 18//
 19// Imported artists start unclaimed; once the admin assigns them to
 20// a wallet (AssignArtist) or the robot verifies a claim, their tracks receive
 21// tips.
 22//
 23// Tips and support are forwarded in the same transaction: this realm ends
 24// every transaction holding no coins.
 25package catalog
 26
 27import (
 28	"chain"
 29	"chain/runtime"
 30	"chain/runtime/unsafe"
 31	"strings"
 32	"time"
 33
 34	"gno.land/p/nym-alexiscolin000/gnoradio/role/v0"
 35	"gno.land/p/nym-alexiscolin000/gnoradio/text/v0"
 36	"gno.land/r/nym-alexiscolin000/gnoradio/data"
 37)
 38
 39const (
 40	version = "1.0.0"
 41	denom   = "ugnot"
 42	ugnot   = 1_000_000
 43
 44	OriginArtist  = "artist"
 45	OriginCurated = "curated"
 46	OriginAudius  = "audius"
 47	OriginJamendo = "jamendo"
 48
 49	minTip         = 100_000           // 0.1 GNOT
 50	maxTip         = 1_000_000 * ugnot // keeps share math far from overflow
 51	maxSplits      = 4
 52	maxSplitPct    = 90 // the artist always keeps at least 10%
 53	earlyBadges    = 10
 54	minDuration    = 10
 55	maxDuration    = 1200
 56	maxPerArtist   = 500 // tracks per artist
 57	maxAlbumTracks = 40
 58	maxListTracks  = 200
 59	maxUserLists   = 50
 60	maxFollows     = 1000
 61	maxUserReports = 5   // open reports per reporter
 62	maxTitle       = 64  // track, album and playlist titles, in characters
 63	maxBio         = 280 // artist bio
 64	maxCredits     = 160 // a track's credits and a curated import's attribution
 65	maxReason      = 200 // report and moderation reasons
 66	topSize        = 10
 67	maxSupportPct  = 50
 68)
 69
 70// NumGenres is the size of the fixed genre list; genre ids are 1..NumGenres.
 71const NumGenres = 20
 72
 73var genres = [NumGenres + 1]string{"",
 74	"Electronica", "Synthwave", "Ambient", "Techno", "House", "Drum & Bass", "Dubstep & Trap",
 75	"Lo-fi Beats", "Hip-hop & Rap", "R&B & Soul", "Rock & Indie", "Metal & Punk", "Pop",
 76	"Jazz & Blues", "Folk & Acoustic", "Cinematic & Classical", "World", "Latin", "Reggae & Dub", "Funk & Disco"}
 77
 78// licenses an artist may pick; curated imports are limited to CC0, BY, BY-SA, BY-NC and BY-NC-SA
 79// (no ND, no reserved rights).
 80var licenses = []string{
 81	"CC0-1.0", "CC-BY-4.0", "CC-BY-SA-4.0", "CC-BY-ND-4.0",
 82	"CC-BY-NC-4.0", "CC-BY-NC-SA-4.0", "CC-BY-NC-ND-4.0", "ALL-RIGHTS-RESERVED",
 83}
 84
 85// AudiusLicense is the license id of tracks streamed from Audius.
 86const AudiusLicense = "Audius-OML"
 87
 88const rightsTerms = "I own or control the rights to this recording and its composition, or hold a licence that allows this, and I grant the operator of GnoRadio and its users a worldwide, non-exclusive, royalty-free licence to store, stream, broadcast and display it through GnoRadio and gno.land for as long as it is published."
 89
 90var (
 91	self  string     // this realm's path
 92	admin *role.Role // the moderator; nil when a release before renounced it
 93)
 94
 95// init takes the admin over from the previous release (catalog/config
 96// "admin"), or names the deployer. While this realm is the catalog writer
 97// (release 1 at deploy) it also makes the catalog's collections.
 98func init(cur realm) {
 99	self = cur.PkgPath()
100	holder, mirrored := data.Get(cConfig, "admin")
101	if !mirrored {
102		deployer := unsafe.OriginCaller()
103		if deployer == "" && runtime.ChainID() == "dev" {
104			deployer = chain.PackageAddress(self + "/dev") // gno test: a key nobody holds
105		}
106		holder = deployer.String()
107	}
108	if holder != "" {
109		admin = role.New("admin", address(holder))
110	}
111	if data.Writer("catalog") == self {
112		setup(cur, address(holder))
113	}
114}
115
116// ---- helpers shared by every file ----
117
118func now() int64 { return time.Now().Unix() }
119
120func userCaller(cur realm) address {
121	if !cur.Previous().IsUserCall() {
122		panic("catalog: call this directly from your wallet")
123	}
124	return cur.Previous().Address()
125}
126
127// adminAddr is the admin: the data realm's mirror is the authority, so a
128// change the previous release made after this one was deployed counts.
129func adminAddr() address {
130	h, ok := data.Get(cConfig, "admin")
131	return role.Mirror(admin, h, ok)
132}
133
134// syncAdmin adopts the mirror when the previous release changed it after
135// this one's init (during the release's delay).
136func syncAdmin() {
137	h, ok := data.Get(cConfig, "admin")
138	admin = role.Adopt(admin, "admin", h, ok)
139}
140
141func onlyAdmin(cur realm) {
142	syncAdmin()
143	if admin == nil || !admin.Is(0, cur) {
144		panic("catalog: admin only")
145	}
146	noPayment()
147}
148
149func noPayment() {
150	if len(unsafe.OriginSend()) != 0 {
151		panic("catalog: this action does not take coins")
152	}
153}
154
155// paid returns the ugnot attached to the call; anything else is refused.
156func paid() int64 {
157	sent := unsafe.OriginSend()
158	if len(sent) != 1 || sent[0].Denom != denom || sent[0].Amount <= 0 {
159		panic("catalog: attach an amount in ugnot")
160	}
161	return sent[0].Amount
162}
163
164func emitEvent(name string, kv ...string) { chain.Emit(name, kv...) }
165
166// ---- release and admin ----
167
168// Ready tells the data realm that this release is on chain (see
169// data.Propose). Anyone may call it.
170func Ready(cur realm) {
171	noPayment()
172	data.Ready(cross(cur))
173}
174
175// OfferAdmin offers the admin role to an address (offering it to the admin
176// cancels the offer); AcceptAdmin takes it. Each change is mirrored to the
177// data realm, so the next release keeps the admin.
178func OfferAdmin(cur realm, to address) {
179	onlyAdmin(cur)
180	admin.Offer(0, cur, to)
181}
182
183func AcceptAdmin(cur realm) {
184	noPayment()
185	syncAdmin()
186	if admin == nil {
187		panic("catalog: nothing offered to you")
188	}
189	admin.Accept(0, cur)
190	data.Set(cross(cur), cConfig, "admin", admin.Holder().String())
191}
192
193// RenounceAdmin gives the admin role up for good, for this release and the
194// next ones.
195func RenounceAdmin(cur realm) {
196	onlyAdmin(cur)
197	admin.Renounce(0, cur)
198	data.Set(cross(cur), cConfig, "admin", "")
199}
200
201// Render points gnoweb visitors to the GnoRadio site; a retired release says
202// which realm took over.
203func Render(_ string) string {
204	home := strings.TrimPrefix(strings.TrimSuffix(self, "catalog/v1"), "gno.land") + "home/v1"
205	d := strings.TrimPrefix(dataPath, "gno.land")
206	out := "# GnoRadio · catalog\n\nThis contract holds GnoRadio's catalog rules; its data lives in [" + d + "](" + d +
207		"). The site lives at [" + home + "](" + home + ").\n"
208	if w := data.Writer("catalog"); w != self {
209		out += "\nThis release is retired: the catalog is now `" + w + "`.\n"
210	}
211	return out
212}
213
214// Info is the catalog's summary for the app: counts, the admin and the
215// realm that writes the catalog now ("current", this one unless retired).
216func Info() string {
217	t := rd()
218	return `{"version":` + text.Str(version) + `,"artists":` + text.Itoa64(t.count(mArtists)) + `,"tracks":` + text.Itoa64(t.count(mTracks)) +
219		`,"albums":` + text.Itoa64(t.count(mAlbums)) + `,"playlists":` + text.Itoa64(t.count(mPlaylists)) + `,"listeners":` + text.Itoa(data.Size(cUsers)) +
220		`,"genres":` + text.Itoa(NumGenres) + `,"now":` + text.Itoa64(now()) + `,"current":` + text.Str(data.Writer("catalog")) +
221		`,"admin":` + text.Str(adminAddr().String()) + `}`
222}