requests.gno
5.89 Kb · 213 lines
1package memba_notes_v1
2
3import (
4 "chain/runtime"
5 "gno.land/p/samcrew/avl"
6 keys "gno.land/r/samcrew/enckeys_v1"
7 "strconv"
8 "strings"
9)
10
11const MaxAccessRequests = 256
12const RequestTTLBlocks int64 = 10000
13
14type accessRequest struct {
15 Who address
16 Expires int64
17 Active bool
18 Status string
19 Last receipt
20}
21
22var requests = avl.NewTree()
23
24func requestKey(id [16]byte, who address) string { return idText(id) + ":" + string(who) }
25func RequestAccess(cur realm, id [16]byte, opID [16]byte) {
26 who := enter(cur, false)
27 defer leave()
28 n := live(id)
29 if !cur.Previous().IsUserCall() || !encrypted(n.Mode) || who == n.Owner || contains(n.Readers, who) {
30 panic("notes: request not applicable")
31 }
32 if opID == ([16]byte{}) {
33 panic("notes: empty operation id")
34 }
35 h := runtime.ChainHeight()
36 if h > int64(9223372036854775807)-RequestTTLBlocks {
37 panic("notes: request height overflow")
38 }
39 key := requestKey(id, who)
40 if v, ok := requests.Get(key); ok {
41 r := v.(*accessRequest)
42 if r.Last.Operation == opID {
43 panic("notes: operation id reused")
44 }
45 if !r.Active && r.Status == "rejected" && r.Expires > h {
46 panic("notes: request rejected recently")
47 }
48 if r.Active {
49 if r.Expires > h {
50 panic("notes: request pending")
51 }
52 n.RequestCount--
53 }
54 }
55 if n.RequestCount >= MaxAccessRequests {
56 panic("notes: request capacity; purge expired requests")
57 }
58 n.RequestCount++
59 requests.Set(key, &accessRequest{who, h + RequestTTLBlocks, true, "pending", receipt{opID, who, h}})
60}
61func closeRequest(n *note, who address, status string, actor address, opID [16]byte) {
62 v, ok := requests.Get(requestKey(n.ID, who))
63 if !ok {
64 panic("notes: request missing")
65 }
66 r := v.(*accessRequest)
67 if !r.Active {
68 panic("notes: request already closed")
69 }
70 if opID == ([16]byte{}) || opID == r.Last.Operation {
71 panic("notes: invalid request operation")
72 }
73 h := runtime.ChainHeight()
74 r.Active = false
75 r.Status = status
76 r.Last = receipt{opID, actor, h}
77 n.RequestCount--
78 // A rejection keeps the record until one full TTL after it, so neither the
79 // requester nor PurgeExpiredRequests can end the re-request cooldown early.
80 if status == "rejected" {
81 if h > int64(9223372036854775807)-RequestTTLBlocks {
82 panic("notes: request height overflow")
83 }
84 r.Expires = h + RequestTTLBlocks
85 }
86}
87func CancelAccessRequest(cur realm, id [16]byte, opID [16]byte) {
88 who := enter(cur, false)
89 defer leave()
90 n := find(id)
91 closeRequest(n, who, "cancelled", who, opID)
92}
93func RejectAccessRequest(cur realm, id [16]byte, to address, opID [16]byte) {
94 who := enter(cur, false)
95 defer leave()
96 mutateRejectAccessRequest(cur, who, id, to, opID)
97}
98func mutateRejectAccessRequest(cur realm, who address, id [16]byte, to address, opID [16]byte) {
99 n := live(id)
100 owner(n, who)
101 closeRequest(n, keys.CanonicalAddress(to), "rejected", who, opID)
102}
103func acknowledgeRequests(n *note, actor address, opID [16]byte) {
104 for _, who := range n.Readers {
105 if v, ok := requests.Get(requestKey(n.ID, who)); ok {
106 r := v.(*accessRequest)
107 if r.Active {
108 r.Active = false
109 r.Status = "granted"
110 r.Last = receipt{opID, actor, runtime.ChainHeight()}
111 n.RequestCount--
112 }
113 }
114 }
115}
116func requestJSON(r *accessRequest) string {
117 return `{"address":` + q(string(r.Who)) + `,"expires_height":` + q(strconv.FormatInt(r.Expires, 10)) + `,"active":` + strconv.FormatBool(r.Active) + `,"status":` + q(r.Status) + `,"op_id":` + q(idText(r.Last.Operation)) + `,"actor":` + q(string(r.Last.Actor)) + `,"height":` + q(strconv.FormatInt(r.Last.Height, 10)) + `}`
118}
119func AccessRequestJSON(idHex string, who address) string {
120 v, ok := requests.Get(requestKey(readID(idHex), keys.CanonicalAddress(who)))
121 if !ok {
122 return "null"
123 }
124 return requestJSON(v.(*accessRequest))
125}
126func requestStart(id [16]byte, cursor string) string {
127 if cursor != "" && string(keys.CanonicalAddress(address(cursor))) != cursor {
128 panic("notes: bad request cursor")
129 }
130 return idText(id) + ":" + cursor
131}
132func AccessRequestsJSON(idHex, cursor string, limit int) string {
133 id := readID(idHex)
134 n := find(id)
135 if limit < 1 || limit > MaxPage {
136 panic("notes: page limit")
137 }
138 start := requestStart(id, cursor)
139 prefix := idText(id) + ":"
140 items := []string{}
141 next := ""
142 requests.Iterate(start, prefix+"\xff", func(k string, v interface{}) bool {
143 if cursor != "" && k == start {
144 return false
145 }
146 items = append(items, requestJSON(v.(*accessRequest)))
147 next = strings.TrimPrefix(k, prefix)
148 return len(items) == limit
149 })
150 if len(items) < limit {
151 next = ""
152 }
153 return `{"items":[` + strings.Join(items, ",") + `],"next_cursor":` + q(next) + `,"active_count":` + u(uint64(n.RequestCount)) + `}`
154}
155
156// At most limit records are inspected, not just removed. The returned cursor
157// lets a caller sweep the full request set without an unbounded scan.
158func PurgeExpiredRequests(cur realm, id [16]byte, cursor string, limit int) string {
159 enter(cur, false)
160 defer leave()
161 n := find(id)
162 if limit < 1 || limit > MaxPage {
163 panic("notes: purge limit")
164 }
165 start := requestStart(id, cursor)
166 prefix := idText(id) + ":"
167 doomed := []string{}
168 next := ""
169 inspected := 0
170 h := runtime.ChainHeight()
171 requests.Iterate(start, prefix+"\xff", func(k string, v interface{}) bool {
172 if cursor != "" && k == start {
173 return false
174 }
175 r := v.(*accessRequest)
176 if r.Expires <= h {
177 doomed = append(doomed, k)
178 if r.Active {
179 n.RequestCount--
180 }
181 }
182 inspected++
183 next = strings.TrimPrefix(k, prefix)
184 return inspected == limit
185 })
186 for _, key := range doomed {
187 requests.Remove(key)
188 }
189 if inspected < limit {
190 next = ""
191 }
192 return next
193}
194
195func purgeRequests(n *note, limit int) int {
196 if limit == 0 {
197 return 0
198 }
199 prefix := idText(n.ID) + ":"
200 doomed := []string{}
201 requests.Iterate(prefix, prefix+"\xff", func(k string, v interface{}) bool {
202 r := v.(*accessRequest)
203 if r.Active {
204 n.RequestCount--
205 }
206 doomed = append(doomed, k)
207 return len(doomed) == limit
208 })
209 for _, k := range doomed {
210 requests.Remove(k)
211 }
212 return len(doomed)
213}