gnotif.gno
7.85 Kb · 250 lines
1// Package gnotif is a registry of notification triggers. Each trigger names
2// a realm and an event type to watch, and the text of the notification that
3// gnotif servers send when a matching event lands on chain. A later registry
4// version follows the rules in
5// https://gnotif.xyz/docs/how-it-works/#rules-for-a-new-registry-version.
6package gnotif
7
8import (
9 "chain"
10 "chain/runtime"
11 "chain/runtime/unsafe"
12 "strings"
13
14 "gno.land/p/g13rqvks880v0hmlfepcj0kkudq0js38d3pc5k6a/gnotif/trigger/v0"
15 "gno.land/p/nt/bptree/v0"
16 "gno.land/p/nt/seqid/v0"
17 "gno.land/r/sys/names"
18)
19
20// Subpath is the subpath of a realm's gnotif identity: me := cur.Sub(gnotif.Subpath).
21const Subpath = "gnotif"
22
23// Trigger is an alias, so a dapp imports only the registry.
24type Trigger = trigger.Trigger
25
26// Handle changes the triggers of the realm that opened it. Each method also
27// needs that realm's gnotif identity as its caller, so the pointer alone is
28// useless to any other realm.
29type Handle struct {
30 target string
31 approver string // the namespace owner who signed the Open
32}
33
34var (
35 triggers bptree.BPTree // id → record.encode()
36 byTarget bptree.BPTree // target → []string of its active ids, oldest first
37 handles bptree.BPTree // target → its live *Handle
38 lastID seqid.ID
39)
40
41// Open returns the handle that changes the triggers of the realm whose gnotif
42// identity calls it. It panics while the realm holds a handle its namespace
43// owner has not revoked, and unless that owner signed the transaction and
44// sent it straight to the realm.
45func Open(cur realm) *Handle {
46 me := cur.Previous()
47 target := targetOf(0, me)
48 if handles.Has(target) {
49 panic(target + " already holds a handle: its namespace owner can Revoke it")
50 }
51 mustOwnerEntered(0, me, target)
52 h := &Handle{target: target, approver: unsafe.OriginCaller().String()}
53 handles.Set(target, h)
54 return h
55}
56
57// Set stores t under name and returns its id. A new name gets a new id. On a
58// name in use, t must watch the same event, filter and param: the id stays,
59// so users who turned the trigger on keep it.
60func (h *Handle) Set(cur realm, name string, t Trigger) string {
61 h.mustHold(cur.Previous().PkgPath())
62 if !validName(name, 1, maxName) {
63 panic("invalid name")
64 }
65 validate(t)
66 r := record{Target: h.target, Name: name, T: t}
67 ids := activeOf(h.target)
68 if id, old, ok := find(ids, name); ok {
69 if old.T == t {
70 return id
71 }
72 if old.T.Event != t.Event || old.T.Filter != t.Filter || old.T.Param != t.Param {
73 panic(name + " watches a different event, filter or param: Remove it, then Set it again")
74 }
75 store(id, r, h.approver)
76 return id
77 }
78 if len(ids) >= maxPerTarget {
79 panic("too many triggers for target")
80 }
81 id := "v1-" + lastID.Next().String()
82 store(id, r, h.approver)
83 byTarget.Set(h.target, append(ids, id))
84 return id
85}
86
87// Remove removes the trigger under name, and with it every user's choice to
88// receive it.
89func (h *Handle) Remove(cur realm, name string) {
90 h.mustHold(cur.Previous().PkgPath())
91 if !validName(name, 1, maxName) {
92 panic("invalid name")
93 }
94 id, _, ok := find(activeOf(h.target), name)
95 if !ok {
96 panic(name + " is not a trigger of " + h.target)
97 }
98 removeIDs(h.target, id)
99}
100
101// mustHold panics unless h is its realm's live handle and caller is that
102// realm's gnotif identity.
103func (h *Handle) mustHold(caller string) {
104 if handles.Get(h.target) != h {
105 panic("not the live handle of " + h.target)
106 }
107 if caller != h.target+"#"+Subpath {
108 panic("not the gnotif identity of " + h.target + ": " + caller)
109 }
110}
111
112// Drop removes target's listed active triggers, ids separated by commas.
113// Spaces around an id are ignored. Only target's namespace owner may call it,
114// directly, with their own key. A call that lists an id more than once, or one
115// that is not an active trigger of target, removes nothing. Drop leaves the
116// realm's handle live: Revoke stops the realm from setting them again.
117func Drop(cur realm, target, ids string) {
118 mustOwn(cur, target)
119 live := make(map[string]bool)
120 for _, id := range activeOf(target) {
121 live[id] = true
122 }
123 dropped := strings.Split(ids, ",")
124 for i, id := range dropped {
125 id = strings.TrimSpace(id)
126 dropped[i] = id
127 if !live[id] {
128 panic(id + " is not an active trigger of " + target)
129 }
130 live[id] = false
131 }
132 removeIDs(target, dropped...)
133}
134
135// Revoke ends target's handle: its methods panic from then on, and target's
136// triggers stay. The realm can Open a new handle. Only target's namespace
137// owner may call it, directly, with their own key.
138func Revoke(cur realm, target string) {
139 mustOwn(cur, target)
140 if _, removed := handles.Remove(target); !removed {
141 panic(target + " holds no handle")
142 }
143}
144
145// Active lists target's active trigger ids, comma-separated, oldest first.
146func Active(target string) string {
147 return strings.Join(activeOf(target), ",")
148}
149
150func activeOf(target string) []string {
151 ids, _ := byTarget.Get(target).([]string)
152 return ids
153}
154
155// ceiling: decodes up to maxPerTarget records per lookup; index target and
156// name to id before raising maxPerTarget.
157func find(ids []string, name string) (string, record, bool) {
158 for _, id := range ids {
159 if r := decodeRecord(triggers.Get(id)); r.Name == name {
160 return id, r, true
161 }
162 }
163 return "", record{}, false
164}
165
166// targetOf returns the realm whose gnotif identity me is, and panics for any
167// other caller.
168func targetOf(_ int, me realm) string {
169 path := me.PkgPath()
170 host, subpath, _ := chain.SplitPkgSubPath(path)
171 if subpath != Subpath || !validTarget(host) {
172 panic("call with this realm's gnotif identity (me := cur.Sub(gnotif.Subpath), then cross(me)); got " + path)
173 }
174 return host
175}
176
177// mustOwnerEntered panics unless target's namespace owner signed the
178// transaction with its own key and called target's realm directly.
179func mustOwnerEntered(_ int, me realm, target string) {
180 if _, _, _, isSession := runtime.GetSessionInfo(); isSession {
181 panic("signed with a session key: sign with the namespace owner's own key")
182 }
183 signer := unsafe.OriginCaller()
184 if ns := namespaceOf(target); !names.IsAuthorizedAddressForNamespace(signer, ns) {
185 panic(signer.String() + " is not authorized for namespace " + ns)
186 }
187 // The Previous of a gnotif identity is the caller of its host realm.
188 entry := me.Previous()
189 if !entry.IsUserCall() || entry.Address() != signer {
190 panic("the dapp was not entered directly by its namespace owner; entered from " + entry.PkgPath())
191 }
192}
193
194// mustOwn panics unless target's namespace owner called straight into the
195// registry with its own key.
196func mustOwn(cur realm, target string) {
197 caller := cur.Previous()
198 if !caller.IsUserCall() {
199 panic("only the namespace owner may do this, in a call signed with its own key; got " + caller.PkgPath())
200 }
201 if _, _, _, isSession := runtime.GetSessionInfo(); isSession {
202 panic("sign with the namespace owner's own key, not a session key")
203 }
204 if ns := namespaceOf(target); !names.IsAuthorizedAddressForNamespace(caller.Address(), ns) {
205 panic(caller.Address().String() + " is not authorized for namespace " + ns)
206 }
207}
208
209// store writes r under id and emits TriggerDeclared.
210func store(id string, r record, approver string) {
211 triggers.Set(id, r.encode())
212 chain.Emit("TriggerDeclared",
213 "id", id,
214 "target", r.Target,
215 "name", r.Name,
216 "event", r.T.Event,
217 "filter", r.T.Filter,
218 "param", r.T.Param,
219 "title", r.T.Title,
220 "body", r.T.Body,
221 "link", r.T.Link,
222 "declarer", chain.PackageAddress(r.Target).String(),
223 "verified", "true",
224 "approver", approver,
225 )
226}
227
228// removeIDs removes the listed active ids of target and emits TriggerRemoved
229// for each.
230func removeIDs(target string, ids ...string) {
231 gone := make(map[string]bool, len(ids))
232 for _, id := range ids {
233 gone[id] = true
234 }
235 var left []string
236 for _, id := range activeOf(target) {
237 if !gone[id] {
238 left = append(left, id)
239 }
240 }
241 if len(left) == 0 {
242 byTarget.Remove(target)
243 } else {
244 byTarget.Set(target, left)
245 }
246 for _, id := range ids {
247 triggers.Remove(id)
248 chain.Emit("TriggerRemoved", "id", id)
249 }
250}