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

artists.gno

6.97 Kb · 223 lines
  1package catalog
  2
  3import (
  4	"strings"
  5
  6	"gno.land/p/nym-alexiscolin000/gnoradio/role/v0"
  7	"gno.land/p/nym-alexiscolin000/gnoradio/store/v0"
  8	"gno.land/p/nym-alexiscolin000/gnoradio/text/v0"
  9	"gno.land/r/nym-alexiscolin000/gnoradio/data"
 10)
 11
 12// RegisterArtist creates or updates the caller's artist profile. Names use
 13// Latin letters so look-alike names in other scripts cannot impersonate.
 14func RegisterArtist(cur realm, name, bio string) int {
 15	caller := userCaller(cur)
 16	noPayment()
 17	name, bio = strings.TrimSpace(name), strings.TrimSpace(bio)
 18	mustText("bio", bio, 0, maxBio)
 19	t := rd()
 20	id := artistOf(t, caller)
 21	if id != 0 {
 22		a := getArtist(t, id)
 23		if name != a.Name { // a kept name stays valid when the reserved list grows
 24			mustName(name)
 25		}
 26		rename(t, a, name)
 27		a.Bio = bio
 28		t.setRec(cArtists, id, fits(artistRecord(a)))
 29		save(cur, t)
 30		emitEvent("ArtistUpdated", "id", text.Itoa(id), "name", name)
 31		return id
 32	}
 33	mustName(name)
 34	a := newArtist(t, OriginArtist, name, bio, "", caller)
 35	t.set(cOwners, caller.String(), text.Itoa(a.ID))
 36	save(cur, t)
 37	emitEvent("ArtistRegistered", "id", text.Itoa(a.ID), "owner", caller.String(), "name", name)
 38	return a.ID
 39}
 40
 41// CreateArtist adds an unclaimed artist. Admin only. A curated artist has a
 42// name and its page at the source (https), shown for attribution. An Audius
 43// or Jamendo artist is a pointer: no name or bio, and source is its platform
 44// id ("audius-user:<id>", "jamendo-artist:<id>"), which also keeps it unique;
 45// the app reads its name live from the platform.
 46func CreateArtist(cur realm, kind, name, bio, source string) int {
 47	onlyAdmin(cur)
 48	name, bio, source = strings.TrimSpace(name), strings.TrimSpace(bio), strings.TrimSpace(source)
 49	switch {
 50	case kind == OriginCurated:
 51		mustName(name)
 52		mustText("bio", bio, 0, maxBio)
 53		if source != "" && !text.HTTPS(source, 300) {
 54			panic("catalog: source must be an https link")
 55		}
 56	case isRef(kind):
 57		if name != "" || bio != "" || !validRef(kind, source) {
 58			panic("catalog: an Audius or Jamendo artist has no name or bio, and source " + refUser(kind) + "<id>")
 59		}
 60	default:
 61		panic("catalog: kind must be curated, audius or jamendo")
 62	}
 63	t := rd()
 64	a := newArtist(t, kind, name, bio, source, "")
 65	save(cur, t)
 66	emitEvent("ArtistCreated", "id", text.Itoa(a.ID), "kind", kind, "name", name)
 67	return a.ID
 68}
 69
 70// AssignArtist hands an unclaimed artist to the wallet that proved it owns
 71// the profile (claim checked off-chain). Their tracks then receive tips.
 72func AssignArtist(cur realm, artistID int, owner address) {
 73	onlyAdmin(cur)
 74	t := rd()
 75	a := mustArtist(t, artistID)
 76	if a.Owner != "" {
 77		panic("catalog: artist already claimed")
 78	}
 79	if !role.Canonical(owner) || data.IsGnoRadio(owner) || artistOf(t, owner) != 0 {
 80		panic("catalog: this wallet is invalid or already has an artist profile")
 81	}
 82	a.Owner, a.Verified = owner, true
 83	putArtist(t, a)
 84	t.del(cClaims, store.Pad(a.ID))
 85	t.set(cOwners, owner.String(), text.Itoa(a.ID))
 86	t.activity("claim", owner, "0", text.Itoa(a.ID), "0")
 87	save(cur, t)
 88	emitEvent("ArtistClaimed", "id", text.Itoa(a.ID), "owner", owner.String())
 89}
 90
 91// Follow and Unfollow an artist.
 92func Follow(cur realm, artistID int) {
 93	caller := userCaller(cur)
 94	noPayment()
 95	t := rd()
 96	a := mustArtist(t, artistID)
 97	if a.Owner == caller {
 98		panic("catalog: you cannot follow yourself")
 99	}
100	u := user(t, caller)
101	follows, ok := store.Add(u.Follows, artistID)
102	if !ok {
103		panic("catalog: already following")
104	}
105	if store.Count(u.Follows) >= maxFollows {
106		panic("catalog: follows limit reached")
107	}
108	u.Follows = follows
109	putUser(t, caller, u)
110	a.Followers++
111	putArtist(t, a)
112	t.activityOnce("follow", caller, "0", text.Itoa(artistID))
113	save(cur, t)
114	emitEvent("Followed", "artist", text.Itoa(artistID), "by", caller.String())
115}
116
117func Unfollow(cur realm, artistID int) {
118	caller := userCaller(cur)
119	noPayment()
120	t := rd()
121	u := user(t, caller)
122	follows, ok := store.Remove(u.Follows, artistID)
123	if !ok {
124		panic("catalog: not following")
125	}
126	u.Follows = follows
127	putUser(t, caller, u)
128	if a := getArtist(t, artistID); a != nil && a.Followers > 0 {
129		a.Followers--
130		putArtist(t, a)
131	}
132	save(cur, t)
133	emitEvent("Unfollowed", "artist", text.Itoa(artistID), "by", caller.String())
134}
135
136func newArtist(t *tx, kind, name, bio, source string, owner address) *Artist {
137	k := skeleton(name)
138	if isRef(kind) {
139		k = source // a pointer is reserved under its platform id ("audius-user:12")
140	}
141	if _, taken := t.get(cNames, k); taken {
142		panic("catalog: this artist name is already taken")
143	}
144	a := &Artist{ID: int(t.bump(mArtists, 1)), Owner: owner, Kind: kind, Name: name, Bio: bio, Source: source, Joined: now()}
145	t.addRec(cArtists, a.ID, fits(artistRecord(a)))
146	t.set(cNames, k, text.Itoa(a.ID))
147	return a
148}
149
150// rename moves the artist to a new name and frees the old one (the caller
151// saves a). A hidden artist keeps its name so it cannot dodge moderation.
152func rename(t *tx, a *Artist, name string) {
153	k, old := skeleton(name), skeleton(a.Name)
154	if k == old {
155		a.Name = name
156		return
157	}
158	if a.Hidden {
159		panic("catalog: a hidden artist cannot change its name")
160	}
161	if id, ok := t.get(cNames, k); ok && text.Atoi(id) != a.ID {
162		panic("catalog: this artist name is already taken")
163	}
164	t.set(cNames, k, text.Itoa(a.ID))
165	if id, ok := t.get(cNames, old); old != "" && ok && text.Atoi(id) == a.ID { // a pointer had no name
166		t.del(cNames, old)
167	}
168	a.Name = name
169	if a.Verified {
170		emitEvent("Verified", "artist", text.Itoa(a.ID), "verified", "false")
171	}
172	a.Verified = false
173	// A pending claim was checked for the old name: a rename must not inherit
174	// it, nor the old proof (a later Verify by the admin shows no host).
175	t.del(cClaims, store.Pad(a.ID))
176	t.del(cProofs, store.Pad(a.ID))
177}
178
179// ReleaseName frees a reserved name no visible artist is using. Admin only.
180func ReleaseName(cur realm, name string) {
181	onlyAdmin(cur)
182	t := rd()
183	k := strings.TrimSpace(name)
184	if strings.HasPrefix(k, refKey) { // a pointer track's key: free once the track is hidden
185		v, ok := t.get(cNames, k)
186		if !ok {
187			panic("catalog: this name is not reserved")
188		}
189		if tr := getTrack(t, text.Atoi(v)); tr != nil && !tr.Hidden {
190			panic("catalog: this pointer track is visible")
191		}
192		t.del(cNames, k)
193		save(cur, t)
194		emitEvent("NameReleased", "name", k)
195		return
196	}
197	ref := strings.HasPrefix(k, refUser(OriginAudius)) || strings.HasPrefix(k, refUser(OriginJamendo)) // a pointer's raw platform key
198	if !ref {
199		k = skeleton(k)
200	}
201	v, ok := t.get(cNames, k)
202	if !ok {
203		panic("catalog: this name is not reserved")
204	}
205	if a := getArtist(t, text.Atoi(v)); a != nil && !a.Hidden && (ref && a.Source == k || !ref && skeleton(a.Name) == k) {
206		panic("catalog: this name belongs to a visible artist")
207	}
208	t.del(cNames, k)
209	save(cur, t)
210	emitEvent("NameReleased", "name", k)
211}
212
213// refUser is the source prefix of a pointer artist's platform id.
214func refUser(kind string) string {
215	if kind == OriginJamendo {
216		return "jamendo-artist:"
217	}
218	return "audius-user:"
219}
220
221func validRef(kind, source string) bool {
222	return strings.HasPrefix(source, refUser(kind)) && alnum(strings.TrimPrefix(source, refUser(kind)), 1, 32)
223}