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}