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

notes.gno

5.27 Kb · 159 lines
  1package radio
  2
  3import (
  4	"chain"
  5	"encoding/hex"
  6	"strconv"
  7	"strings"
  8
  9	"gno.land/p/nym-alexiscolin000/gnoradio/role/v0"
 10	"gno.land/p/nym-alexiscolin000/gnoradio/store/v0"
 11	"gno.land/p/nym-alexiscolin000/gnoradio/text/v0"
 12)
 13
 14// Dedications: reports, strikes and mutes. A dedication's reports are
 15// radio/notes P(station)/T(start) -> the reporters' addresses, each followed
 16// by ","; once hidden, struckMark, the strike time it gave its author and
 17// the one before ("" none), so that RestoreNote takes back this strike only.
 18
 19const struckMark = "struck:"
 20
 21func noteKey(stationID int, start int64) string {
 22	return store.Pad(stationID) + "/" + store.TimeKey(start)
 23}
 24
 25// ReportNote flags the dedication of the slot starting at start. Only a
 26// listener with the reputation sponsored picks need (a first pick a week
 27// old, 3 normal picks this 15-day period and the one before: sponsorBlock) may report, so
 28// throwaway wallets cannot; the third such listener hides it at once, with
 29// no moderator involved. A first hidden dedication is a strike; a second
 30// within muteFor pauses its author's dedications for muteFor (the admin can
 31// lift it: Unmute).
 32func ReportNote(cur realm, stationID int, start int64) {
 33	caller := userCaller(cur)
 34	noPayment()
 35	t := rd()
 36	st, s := noteSlot(t, stationID, start)
 37	if s.NoteHidden {
 38		panic("radio: no dedication at this time on this station")
 39	}
 40	if s.By == caller {
 41		panic("radio: you cannot report your own dedication")
 42	}
 43	if sponsorBlock(t, caller) != "" {
 44		panic("radio: reporting needs a first pick 7 days old and 3 normal picks in the last two 15-day periods")
 45	}
 46	key := noteKey(st.id, start)
 47	by := t.val(cNotes, key)
 48	if strings.Contains(by, caller.String()) {
 49		panic("radio: you already reported this dedication")
 50	}
 51	by += caller.String() + ","
 52	if strings.Count(by, ",") < hideAfter {
 53		t.set(cNotes, key, by)
 54		save(cur, t)
 55		chain.Emit("NoteReported", "station", text.Itoa(st.id), "start", text.Itoa64(start), "by", caller.String())
 56		return
 57	}
 58	s.NoteHidden, s.body, st.dirty = true, "", true
 59	ts, author := now(), s.By.String()
 60	last, struck := t.get(cStrikes, author)
 61	mute := struck && ts-text.MustAtoi64(last) < muteFor
 62	if mute {
 63		t.set(cMuted, author, text.Itoa64(ts+muteFor))
 64	}
 65	t.set(cStrikes, author, text.Itoa64(ts))
 66	t.set(cNotes, key, struckMark+text.Itoa64(ts)+":"+last)
 67	save(cur, t)
 68	chain.Emit("NoteHidden", "station", text.Itoa(st.id), "start", text.Itoa64(start), "author", author, "muted", strconv.FormatBool(mute))
 69}
 70
 71// RestoreNote shows again a dedication hidden by reports by mistake and
 72// clears its author's strike (Unmute lifts a pause). Radio admin only.
 73func RestoreNote(cur realm, stationID int, start int64) {
 74	onlyAdmin(cur)
 75	t := rd()
 76	st, s := noteSlot(t, stationID, start)
 77	if !s.NoteHidden {
 78		panic("radio: this dedication is not hidden") // its pending reports stay
 79	}
 80	s.NoteHidden, s.body, st.dirty = false, "", true
 81	key := noteKey(st.id, start)
 82	// A wrong hide is no strike: take back the one it gave, if it is still
 83	// the author's last, and keep any earlier one.
 84	if rec := t.val(cNotes, key); strings.HasPrefix(rec, struckMark) {
 85		gave, before, _ := strings.Cut(rec[len(struckMark):], ":")
 86		author := s.By.String()
 87		if t.val(cStrikes, author) == gave {
 88			if before == "" {
 89				t.del(cStrikes, author)
 90			} else {
 91				t.set(cStrikes, author, before)
 92			}
 93		}
 94	}
 95	t.del(cNotes, key)
 96	save(cur, t)
 97	chain.Emit("NoteRestored", "station", text.Itoa(st.id), "start", text.Itoa64(start))
 98}
 99
100// SetModBot names the moderation robot by its Ed25519 public key in hex
101// ("" turns dedications off). Radio admin only.
102func SetModBot(cur realm, pubKeyHex string) {
103	onlyAdmin(cur)
104	key, err := hex.DecodeString(pubKeyHex)
105	if err != nil || (len(key) != 0 && len(key) != 32) {
106		panic("radio: the robot key is a 32-byte Ed25519 public key in hex")
107	}
108	t := rd()
109	t.set(cConfig, "modbot", hex.EncodeToString(key))
110	save(cur, t)
111	chain.Emit("ModBotSet", "key", pubKeyHex)
112}
113
114// MutedUntil returns the unix time until which who's dedications are paused
115// after reports, 0 when they are not: the moderation robot refuses to sign
116// for a muted author.
117func MutedUntil(who address) int64 {
118	if v := rd().val(cMuted, who.String()); v != "" {
119		if until := text.MustAtoi64(v); now() < until {
120			return until
121		}
122	}
123	return 0
124}
125
126// Unmute lets a muted listener dedicate again before the week ends, e.g.
127// after reports by throwaway wallets. Radio admin only.
128func Unmute(cur realm, who address) {
129	onlyAdmin(cur)
130	if !role.Canonical(who) {
131		panic("radio: not an address (lower-case g1...)")
132	}
133	t := rd()
134	if !t.del(cMuted, who.String()) {
135		panic("radio: this address is not muted")
136	}
137	save(cur, t)
138	chain.Emit("Unmuted", "address", who.String())
139}
140
141// noteSlot finds the dedicated slot starting at start. On Main between its
142// own picks it is the genre station's slot Main simulcast: the genre of the
143// hour the slot began in, or of now when the hour changed under it.
144func noteSlot(t *tx, stationID int, start int64) (*station, *Slot) {
145	st := t.station(stationID)
146	cands := []*station{st}
147	if _, own := st.playing(start); !own {
148		cands = []*station{st.relay(start), st.relay(now())}
149	}
150	for _, c := range cands {
151		sl := c.slots()
152		for i := range sl {
153			if s := &sl[i]; s.Start == start && s.Note != "" {
154				return c, s
155			}
156		}
157	}
158	panic("radio: no dedication at this time on this station")
159}