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

moderation.gno

8.83 Kb · 312 lines
  1package catalog
  2
  3import (
  4	"strconv"
  5	"strings"
  6
  7	"gno.land/p/nym-alexiscolin000/gnoradio/store/v0"
  8	"gno.land/p/nym-alexiscolin000/gnoradio/text/v0"
  9)
 10
 11// Report flags a track, album, artist or playlist for the moderators
 12// (e.g. "broken link", "not the real artist", "copyright").
 13func Report(cur realm, kind string, target int, reason string) {
 14	caller := userCaller(cur)
 15	noPayment()
 16	reason = strings.TrimSpace(reason)
 17	mustText("reason", reason, 4, maxReason)
 18	t := rd()
 19	switch kind {
 20	case "track":
 21		mustTrack(t, target)
 22	case "album":
 23		if al := getAlbum(t, target); al == nil || al.Hidden {
 24			panic("catalog: unknown album")
 25		}
 26	case "artist":
 27		mustArtist(t, target)
 28	case "playlist":
 29		if p := getPlaylist(t, target); p == nil || p.Hidden {
 30			panic("catalog: unknown playlist")
 31		}
 32	default:
 33		panic("catalog: kind must be track, album, artist or playlist")
 34	}
 35	u := user(t, caller)
 36	if u.Reports >= maxUserReports {
 37		panic("catalog: you have " + text.Itoa(maxUserReports) + " open reports, wait for the moderators")
 38	}
 39	u.Reports++
 40	putUser(t, caller, u)
 41	t.bump(mOpen, 1)
 42	t.put(cReports, store.Pad(int(t.bump(mReports, 1))), store.Rec(kind, text.Itoa(target), reason, caller.String(), text.Itoa64(now())))
 43	save(cur, t)
 44	emitEvent("Reported", "kind", kind, "target", text.Itoa(target), "by", caller.String())
 45}
 46
 47func getReport(t *tx, id int) (ReportInfo, bool) {
 48	rec, ok := t.get(cReports, store.Pad(id))
 49	if !ok || id < 1 {
 50		return ReportInfo{}, false
 51	}
 52	f := store.Fields(rec)
 53	return ReportInfo{ID: id, Kind: f[0], Target: f[1], Reason: f[2], By: address(f[3]), At: text.Atoi64(f[4])}, true
 54}
 55
 56// ResolveReport closes a report once handled. Admin only.
 57func ResolveReport(cur realm, id int) {
 58	onlyAdmin(cur)
 59	t := rd()
 60	if !resolve(t, id) {
 61		panic("catalog: unknown report")
 62	}
 63	save(cur, t)
 64}
 65
 66// ResolveReports closes the open reports with ids from..from+count-1 (count
 67// 1-50) in one transaction, e.g. a run of spam reports; ids already closed
 68// are skipped. There is no global cap on open reports, so spam never blocks
 69// an honest reporter: each wallet keeps at most 5 open. Admin only.
 70func ResolveReports(cur realm, from, count int) {
 71	onlyAdmin(cur)
 72	if count < 1 || count > 50 {
 73		panic("catalog: resolve 1 to 50 reports at a time")
 74	}
 75	t := rd()
 76	for id := from; id < from+count; id++ {
 77		resolve(t, id)
 78	}
 79	save(cur, t)
 80}
 81
 82// resolve closes one report, false when there is no such open report.
 83func resolve(t *tx, id int) bool {
 84	r, ok := getReport(t, id)
 85	if !ok {
 86		return false
 87	}
 88	t.del(cReports, store.Pad(id))
 89	if t.count(mOpen) > 0 {
 90		t.bump(mOpen, -1)
 91	}
 92	if u := user(t, r.By); u.Reports > 0 {
 93		u.Reports--
 94		putUser(t, r.By, u)
 95	}
 96	emitEvent("ReportResolved", "id", text.Itoa(id))
 97	return true
 98}
 99
100// Hide or restore content. Admin only; hiding takes a public reason (4-200
101// characters) and restoring works on anything, whoever hid it. Hidden content
102// stays on-chain (and on IPFS); it is no longer listed, played or tippable.
103// Radio stations drop a hidden track on their next Refresh
104// (radio.RefreshArtist for a whole artist).
105func HideTrack(cur realm, trackID int, hidden bool, reason string) {
106	onlyAdmin(cur)
107	t := rd()
108	tr := getTrack(t, trackID)
109	if tr == nil {
110		panic("catalog: unknown track")
111	}
112	tr.Hidden = hidden
113	putTrack(t, tr)
114	moderate(cur, t, "track", trackID, hidden, reason)
115}
116
117func HideAlbum(cur realm, albumID int, hidden bool, reason string) {
118	onlyAdmin(cur)
119	t := rd()
120	al := getAlbum(t, albumID)
121	if al == nil {
122		panic("catalog: unknown album")
123	}
124	al.Hidden = hidden
125	putAlbum(t, al)
126	moderate(cur, t, "album", albumID, hidden, reason)
127}
128
129func HideArtist(cur realm, artistID int, hidden bool, reason string) {
130	onlyAdmin(cur)
131	t := rd()
132	a := getArtist(t, artistID)
133	if a == nil {
134		panic("catalog: unknown artist")
135	}
136	a.Hidden = hidden
137	putArtist(t, a)
138	moderate(cur, t, "artist", artistID, hidden, reason)
139}
140
141func HidePlaylist(cur realm, playlistID int, hidden bool, reason string) {
142	onlyAdmin(cur)
143	t := rd()
144	p := getPlaylist(t, playlistID)
145	if p == nil {
146		panic("catalog: unknown playlist")
147	}
148	p.Hidden = hidden
149	putPlaylist(t, p)
150	moderate(cur, t, "playlist", playlistID, hidden, reason)
151}
152
153// moderate records the moderator's public reason and saves t.
154func moderate(cur realm, t *tx, kind string, id int, hidden bool, reason string) {
155	mark(t, kind, id, hidden, reason)
156	save(cur, t)
157}
158
159// mark records the moderator's public reason for one item (the caller saves).
160func mark(t *tx, kind string, id int, hidden bool, reason string) {
161	reason = strings.TrimSpace(reason)
162	k := kind + ":" + text.Itoa(id)
163	if hidden {
164		mustText("reason", reason, 4, maxReason)
165		t.set(cRemoved, k, reason)
166	} else {
167		t.del(cRemoved, k)
168	}
169	emitEvent("Moderated", "kind", kind, "target", text.Itoa(id), "hidden", strconv.FormatBool(hidden), "reason", reason, "by", "moderator")
170}
171
172// maxHideBatch bounds HideSource: 2 writes per artist.
173const maxHideBatch = 50
174
175// HideSource hides every artist read from a platform ("audius" or
176// "jamendo"), and so all their tracks, for a platform that ends GnoRadio's
177// API access. It walks artist ids from `from`, count (1-50) at a time, and
178// returns where to go on (0 once past the last artist). One artist opting out
179// is a HideArtist. Admin only.
180func HideSource(cur realm, kind string, from, count int, reason string) int {
181	onlyAdmin(cur)
182	if !isRef(kind) || from < 1 || count < 1 || count > maxHideBatch {
183		panic("catalog: HideSource(audius|jamendo, from >= 1, count 1-" + text.Itoa(maxHideBatch) + ", reason)")
184	}
185	mustText("reason", strings.TrimSpace(reason), 4, maxReason)
186	t := rd()
187	last := int(t.count(mArtists))
188	for id := from; id < from+count && id <= last; id++ {
189		if a := getArtist(t, id); a != nil && a.Kind == kind {
190			// An artist that hid itself gets the reason too, so HideOwn cannot show it again;
191			// one a moderator hid keeps its own reason.
192			if r, ok := t.get(cRemoved, "artist:"+text.Itoa(id)); a.Hidden && ok && r != "" {
193				continue
194			}
195			if !a.Hidden {
196				a.Hidden = true
197				putArtist(t, a)
198			}
199			mark(t, "artist", id, true, reason)
200		}
201	}
202	save(cur, t)
203	if from+count > last {
204		return 0
205	}
206	return from + count
207}
208
209// HideOwn hides or shows again the caller's own track, album or artist
210// profile ("artist", with the profile's id). The artist can show again only
211// what they hid themselves: what the moderator hid stays hidden.
212func HideOwn(cur realm, kind string, id int, hidden bool) {
213	caller := userCaller(cur)
214	noPayment()
215	t := rd()
216	a := getArtist(t, artistOf(t, caller))
217	if a == nil {
218		panic("catalog: create your artist profile first")
219	}
220	k := kind + ":" + text.Itoa(id)
221	if reason, ok := t.get(cRemoved, k); ok && reason != "" {
222		panic("catalog: hidden by the moderator; contact them to restore it")
223	}
224	switch kind {
225	case "track":
226		tr := getTrack(t, id)
227		if tr == nil || tr.Artist != a.ID {
228			panic("catalog: not your track")
229		}
230		mustToggle(tr.Hidden, hidden)
231		tr.Hidden = hidden
232		putTrack(t, tr)
233	case "album":
234		al := getAlbum(t, id)
235		if al == nil || al.Artist != a.ID {
236			panic("catalog: not your album")
237		}
238		mustToggle(al.Hidden, hidden)
239		al.Hidden = hidden
240		putAlbum(t, al)
241	case "artist":
242		if id != a.ID {
243			panic("catalog: not your profile")
244		}
245		mustToggle(a.Hidden, hidden)
246		a.Hidden = hidden
247		putArtist(t, a)
248	default:
249		panic("catalog: kind must be track, album or artist")
250	}
251	if hidden {
252		t.set(cRemoved, k, "")
253	} else {
254		t.del(cRemoved, k)
255	}
256	save(cur, t)
257	emitEvent("Moderated", "kind", kind, "target", text.Itoa(id), "hidden", strconv.FormatBool(hidden), "reason", "", "by", "artist")
258}
259
260func mustToggle(was, hidden bool) {
261	if was == hidden {
262		if hidden {
263			panic("catalog: already hidden")
264		}
265		panic("catalog: already shown")
266	}
267}
268
269// HiddenNote says why a track, album, artist or playlist is hidden, for its
270// page: "Removed by moderation: <reason>", "Hidden by the artist.", or "".
271func HiddenNote(kind string, id int) string {
272	reason, ok := rd().get(cRemoved, kind+":"+text.Itoa(id))
273	switch {
274	case !ok:
275		return ""
276	case reason == "":
277		return "Hidden by the artist."
278	}
279	return "Removed by moderation: " + reason
280}
281
282// Verify marks an artist whose identity was checked off-chain.
283func Verify(cur realm, artistID int, verified bool) {
284	onlyAdmin(cur)
285	t := rd()
286	a := mustArtist(t, artistID)
287	a.Verified = verified
288	putArtist(t, a)
289	t.del(cClaims, store.Pad(artistID)) // settled by the admin either way
290	if !verified {
291		t.del(cProofs, store.Pad(artistID))
292	}
293	save(cur, t)
294	emitEvent("Verified", "artist", text.Itoa(artistID), "verified", strconv.FormatBool(verified))
295}
296
297// AllowHost adds or removes an https host that audio and covers may use.
298func AllowHost(cur realm, host string, allowed bool) {
299	onlyAdmin(cur)
300	host = strings.ToLower(strings.TrimSpace(host))
301	if !validHost(host) {
302		panic("catalog: invalid host")
303	}
304	t := rd()
305	if allowed {
306		t.set(cHosts, host, "1")
307	} else {
308		t.del(cHosts, host)
309	}
310	save(cur, t)
311	emitEvent("HostAllowed", "host", host, "allowed", strconv.FormatBool(allowed))
312}