Skip to content Search Apps Documentation Source Content File Folder Download Copy Actions State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

Community realm. Read the code first. Community realm, deployed by its author. Read the code before you interact or send coins.

gnotif.gno

5.40 Kb · 215 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.
  4package gnotif
  5
  6import (
  7	"chain"
  8	"strconv"
  9	"strings"
 10
 11	"gno.land/p/nt/avl/v0"
 12	"gno.land/p/nt/seqid/v0"
 13)
 14
 15// Trigger is one declared notification rule. Filter holds comma-separated
 16// key=value pairs; Title, Body and Link are templates where {key} stands for
 17// the value of the event's attribute key.
 18type Trigger struct {
 19	ID, Target, Event, Filter, Param, Title, Body, Link string
 20	Declarer                                            address
 21	Verified                                            bool
 22}
 23
 24const (
 25	targetPrefix   = "gno.land/r/"
 26	maxTarget      = 256
 27	maxName        = 64
 28	maxFilterPairs = 8
 29	maxBody        = 255
 30	maxLink        = 256
 31	maxPerTarget   = 64
 32)
 33
 34var (
 35	triggers avl.Tree // id → *Trigger
 36	byTarget avl.Tree // target → *avl.Tree of id → *Trigger, verified triggers only
 37	lastID   seqid.ID
 38)
 39
 40// Declare stores a trigger and returns its id. The trigger is verified when
 41// its caller is the target realm itself.
 42func Declare(cur realm, target, event, filter, param, title, body, link string) string {
 43	validate(target, event, filter, param, title, body, link)
 44	caller := cur.Previous()
 45	verified := caller.IsCode() && caller.PkgPath() == target
 46	var own *avl.Tree
 47	if v := byTarget.Get(target); v != nil {
 48		own = v.(*avl.Tree)
 49	}
 50	if verified && own != nil && own.Size() >= maxPerTarget {
 51		panic("too many triggers for target")
 52	}
 53	tr := &Trigger{
 54		ID:       lastID.Next().String(),
 55		Target:   target,
 56		Event:    event,
 57		Filter:   filter,
 58		Param:    param,
 59		Title:    title,
 60		Body:     body,
 61		Link:     link,
 62		Declarer: caller.Address(),
 63		Verified: verified,
 64	}
 65	triggers.Set(tr.ID, tr)
 66	if verified {
 67		if own == nil {
 68			own = avl.NewTree()
 69			byTarget.Set(target, own)
 70		}
 71		own.Set(tr.ID, tr)
 72	}
 73	chain.Emit("TriggerDeclared",
 74		"id", tr.ID,
 75		"target", tr.Target,
 76		"event", tr.Event,
 77		"filter", tr.Filter,
 78		"param", tr.Param,
 79		"title", tr.Title,
 80		"body", tr.Body,
 81		"link", tr.Link,
 82		"declarer", tr.Declarer.String(),
 83		"verified", strconv.FormatBool(tr.Verified),
 84	)
 85	return tr.ID
 86}
 87
 88// Remove deletes a trigger. Only its declarer may remove it.
 89func Remove(cur realm, id string) {
 90	v := triggers.Get(id)
 91	if v == nil {
 92		panic("unknown trigger")
 93	}
 94	tr := v.(*Trigger)
 95	if tr.Declarer != cur.Previous().Address() {
 96		panic("not the declarer")
 97	}
 98	triggers.Remove(id)
 99	if tr.Verified {
100		own := byTarget.Get(tr.Target).(*avl.Tree)
101		own.Remove(id)
102		if own.Size() == 0 {
103			byTarget.Remove(tr.Target)
104		}
105	}
106	chain.Emit("TriggerRemoved", "id", id)
107}
108
109func validate(target, event, filter, param, title, body, link string) {
110	switch {
111	case !validTarget(target):
112		panic("invalid target")
113	case !validName(event, 1, maxName):
114		panic("invalid event")
115	case !validFilter(filter):
116		panic("invalid filter")
117	case !validName(param, 0, maxName):
118		panic("invalid param")
119	case !sized(title, 1, maxName):
120		panic("invalid title")
121	case !sized(body, 0, maxBody):
122		panic("invalid body")
123	case !validLink(link):
124		panic("invalid link")
125	}
126}
127
128func sized(s string, lo, hi int) bool {
129	return len(s) >= lo && len(s) <= hi
130}
131
132// validTarget accepts a realm path whose segments each match the chain's
133// Re_name, so a target names a path where a realm can exist.
134func validTarget(target string) bool {
135	if !strings.HasPrefix(target, targetPrefix) || len(target) > maxTarget {
136		return false
137	}
138	rest := target[len(targetPrefix):]
139	start := 0 // index of the current segment's first byte
140	for i := 0; i <= len(rest); i++ {
141		if i == len(rest) || rest[i] == '/' {
142			if i == start || !isLowerAlnum(rest[i-1]) {
143				return false
144			}
145			start = i + 1
146			continue
147		}
148		c := rest[i]
149		switch {
150		case i == start:
151			if !(c >= 'a' && c <= 'z') {
152				return false
153			}
154		case c == '_' || c == '-':
155			if !isLowerAlnum(rest[i-1]) {
156				return false
157			}
158		case !isLowerAlnum(c):
159			return false
160		}
161	}
162	return true
163}
164
165func isLowerAlnum(c byte) bool {
166	return c >= 'a' && c <= 'z' || c >= '0' && c <= '9'
167}
168
169// validName accepts identifier bytes only, so a name cannot carry spaces or
170// symbols that imitate the verified mark in rendered output.
171func validName(s string, lo, hi int) bool {
172	if !sized(s, lo, hi) {
173		return false
174	}
175	for i := 0; i < len(s); i++ {
176		c := s[i]
177		if !(c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z' || c >= '0' && c <= '9' || c == '_') {
178			return false
179		}
180	}
181	return true
182}
183
184func validFilter(filter string) bool {
185	if filter == "" {
186		return true
187	}
188	pairs := strings.Split(filter, ",")
189	if len(pairs) > maxFilterPairs {
190		return false
191	}
192	for _, pair := range pairs {
193		key, value, ok := strings.Cut(pair, "=")
194		if !ok || !validName(key, 1, maxName) || !sized(value, 1, maxName) || strings.Contains(value, "=") {
195			return false
196		}
197	}
198	return true
199}
200
201// validLink accepts only a path, so a click always stays on the dapp's origin.
202// "//host" starts with a slash yet resolves to another host. A URL parser
203// drops tab, LF and CR and reads a backslash as a slash, so "/\t/host" and
204// "/\host" would do the same; control bytes and backslashes are refused.
205func validLink(link string) bool {
206	if !sized(link, 1, maxLink) || !strings.HasPrefix(link, "/") || strings.HasPrefix(link, "//") {
207		return false
208	}
209	for i := 0; i < len(link); i++ {
210		if c := link[i]; c < 0x20 || c == 0x7f || c == '\\' {
211			return false
212		}
213	}
214	return true
215}