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