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

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