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}