collect.bend
cswap-bend-cli@0.1.0.0 · 301 lines · 55 definitions
1import Base
2import cswap-bend-cli@0.1.0.0/urec.bend as Urec
3import cswap-bend-cli@0.1.0.0/pollrow.bend as Pollrow
4import cswap-bend-cli@0.1.0.0/usage/cred.bend as UCred
5import cswap-bend-cli@0.1.0.0/store/io.bend as SIo
6import cswap-bend-cli@0.1.0.0/store/seq.bend as SSeq
7import cswap-bend-cli@0.1.0.0/store/base.bend as SBase
8import cswap-bend-cli@0.1.0.0/json.bend as Json
9import cswap-bend-cli@0.1.0.0/clock.bend as Clock
10import cswap-bend-cli@0.1.0.0/fs.bend as Fs
11import cswap-bend-cli@0.1.0.0/act.bend as Act
12import cswap-bend-cli@0.1.0.0/poll.bend as Poll
13import cswap-bend-cli@0.1.0.0/ustore.bend as Ustore
14import cswap-bend-cli@0.1.0.0/usage.bend as Usage
15import cswap-bend-cli@0.1.0.0/view.bend as View
16import cswap-bend-cli@0.1.0.0/store.bend as Store
17
18
19
20
21
22
23
24
25
26
27
28
29
30def ActM(-A: Type) -> Type:
31 Act.of(A)
32
33def ActM.pure(-A: Type, x: A) -> Act.of(A):
34 Act.pure(A, x)
35
36def ActM.bind(-A: Type, -B: Type, m: Act.of(A), f: A -> Act.of(B)) -> Act.of(B):
37 Act.bind(A, B, m, f)
38
39
40
41type Info is Data:
42 Info{num: U32, email: String, org_name: String, org_uuid: String, alias: String, disabled: Bool,
43 active: Bool, creds: String}
44
45def id_of(+i: Info) -> Ustore.Id:
46 match i:
47 case Info{num, email, org_name, org_uuid, alias, disabled, active, creds}:
48 Ustore.Id{U32.show(num), email, org_uuid}
49
50def slot_of(+i: Info) -> String:
51 match i:
52 case Info{num, email, org_name, org_uuid, alias, disabled, active, creds}:
53 U32.show(num)
54
55def read_text2(r: Fs.Res(String)) -> String:
56 match r:
57 case Fail{e}:
58 ""
59 case Done{t}:
60 t
61
62
63def read_text(+path: String) -> IO(String):
64 IO.bind(Fs.Res(String), String, Fs.read_file(path), r => IO.pure(String, read_text2(r)))
65
66def live_ident(+ctx: SBase.Ctx) -> IO(Store.Ident):
67 IO.bind(String, Store.Ident, read_text(SBase.global_config_path(ctx)), t => IO.pure(Store.Ident, Store.ident_of(Json.unwrap(Json.parse(t)))))
68
69def active.pick(ok: Bool, +seq: Json.Json, +id: Store.Ident) -> Maybe<&2, String>:
70 match ok:
71 case True{}:
72 SSeq.find_slot(seq, Store.id_email(id), Store.id_org(id))
73 case False{}:
74 None{}
75
76def active_slot(+seq: Json.Json, +id: Store.Ident) -> Maybe<&2, String>:
77 active.pick(SBase.not_(String.is_empty(Store.id_email(id))), seq, id)
78
79def is_active(m: Maybe<&2, String>, +slot: String) -> Bool:
80 match m:
81 case Some{s}:
82 String.eq(s, slot)
83 case None{}:
84 False{}
85
86def jstr_of(+rec: Json.Json, +k: String) -> String:
87 SSeq.field_str(rec, k)
88
89def flag.of(m: Maybe<&2, Json.Json>) -> Bool:
90 match m:
91 case Some{Json.JBool{True{}}}:
92 True{}
93 case _:
94 False{}
95
96def rec_flag(+rec: Json.Json, +k: String) -> Bool:
97 flag.of(Json.get(k, rec))
98
99def info.mk(+n: U32, +rec: Json.Json, active: Bool, +creds: String) -> Info:
100 Info{n, jstr_of(rec, "email"), jstr_of(rec, "organizationName"), jstr_of(rec, "organizationUuid"), jstr_of(rec, "alias"), rec_flag(rec, "disabled"), active, creds}
101
102def info.creds(active: Bool, +live: String, +root: String, +slot: String, +email: String) -> Act.of(String):
103 match active:
104 case True{}:
105 Act.pure(String, live)
106 case False{}:
107 SIo.read_cred(root, slot, email)
108
109def info.rec(m: Maybe<&2, Json.Json>, +n: U32, +active: Bool, +live: String, +root: String) -> Act.of(Info):
110 match m:
111 case None{}:
112 Act.fail(Info, 1, "Account-" ++ U32.show(n) ++ " has no record")
113 case Some{+rec}:
114 Act.bind(String, Info, info.creds(active, live, root, U32.show(n), jstr_of(rec, "email")), c => Act.pure(Info, info.mk(n, rec, active, c)))
115
116def info.one(+seq: Json.Json, +n: U32, am: Maybe<&2, String>, +live: String, +root: String) -> Act.of(Info):
117 info.rec(SSeq.slot_record(seq, U32.show(n)), n, is_active(am, U32.show(n)), live, root)
118
119def infos.go(ns: List<&2, U32>, +seq: Json.Json, +am: Maybe<&2, String>, +live: String, +root: String) -> Act.of(List<&2, Info>):
120 match ns:
121 case Nil{}:
122 Act.pure(List<&2, Info>, [])
123 case Con{n, t}:
124 Act.bind(Info, List<&2, Info>, info.one(seq, n, am, live, root),
125 i => Act.bind(List<&2, Info>, List<&2, Info>, infos.go(t, seq, am, live, root), rest => Act.pure(List<&2, Info>, i <> rest)))
126
127def gather.go(+ctx: SBase.Ctx, +seq: Json.Json, +id: Store.Ident, +live: String) -> Act.of(List<&2, Info>):
128 infos.go(SSeq.seq_nums(seq), seq, active_slot(seq, id), live, SBase.root_of(ctx))
129
130
131
132def gather(+ctx: SBase.Ctx, +seq: Json.Json) -> Act.of(List<&2, Info>):
133 Act.bind(Store.Ident, List<&2, Info>, Act.lift(Store.Ident, live_ident(ctx)),
134 id => Act.bind(String, List<&2, Info>, Act.lift(String, read_text(SBase.live_creds_path(ctx))), live => gather.go(ctx, seq, id, live)))
135
136def sha.of2(oe: String & String) -> String:
137 (o, e) = oe
138 String.take(o, 64n)
139
140def sha.of1(r: U32 & (String & String)) -> String:
141 (st, oe) = r
142 sha.of2(oe)
143
144def sha.of(r: Result<&1, &1, U32 & String, U32 & (String & String)>) -> String:
145 match r:
146 case Fail{e}:
147 ""
148 case Done{v}:
149 sha.of1(v)
150
151
152def sha256(+text: String) -> IO(String):
153 IO.bind(Result<&1, &1, U32 & String, U32 & (String & String)>, String, Process.run("shasum", ["-a", "256"], text, 4096, 5000), r => IO.pure(String, sha.of(r)))
154
155def fp.rt(+tag: String, +text: String) -> IO(String):
156 IO.bind(String, String, sha256(text), h => IO.pure(String, tag ++ h))
157
158def fp.pick(m: Maybe<&2, String>, +creds: String) -> IO(String):
159 match m:
160 case Some{rt}:
161 fp.rt("sha256:", rt)
162 case None{}:
163 fp.rt("sha256-full:", creds)
164
165
166
167def fingerprint(+creds: String) -> IO(String):
168 fp.pick(UCred.refresh_token(UCred.oauth.of(creds)), creds)
169
170
171def dead.cmp(+stored: String, struck: Maybe<&2, String>) -> U32:
172 match struck:
173 case None{}:
174 1
175 case Some{s}:
176 Clock.sel(String.eq(s, stored), 1, 2)
177
178def dead.go(has: Bool, struck: Maybe<&2, String>, +creds: String) -> IO(U32):
179 match has:
180 case False{}:
181 IO.pure(U32, 0)
182 case True{}:
183 IO.bind(String, U32, fingerprint(creds), f => IO.pure(U32, dead.cmp(f, struck)))
184
185def dead.fp(strikes: U32, struck: Maybe<&2, String>, +creds: String) -> IO(U32):
186 dead.go(U32.is_ge(strikes, 1), struck, creds)
187
188def dead.row(+r: Pollrow.Row, +creds: String) -> IO(U32):
189 match r:
190 case Pollrow.Row{fetched, lg, la, fl, le, bu, np, iv, l429, st, sfp, cu, hu}:
191 dead.fp(st, sfp, creds)
192
193def dead_state(+row: Json.Json, +creds: String) -> IO(U32):
194 dead.row(Pollrow.row.of(row), creds)
195
196def static.cred(none: Bool) -> Maybe<&2, String>:
197 match none:
198 case True{}:
199 Some{"no_credentials"}
200 case False{}:
201 None{}
202
203def static.pick(api: Bool, +creds: String) -> Maybe<&2, String>:
204 match api:
205 case True{}:
206 Some{"api_key"}
207 case False{}:
208 static.cred(Maybe.is_none(&2, String, UCred.access_token(UCred.oauth.of(creds))))
209
210def static_of(+creds: String) -> Maybe<&2, String>:
211 static.pick(SBase.looks_like_api_key(creds), creds)
212
213def held_row(+row: Json.Json, +now: U32) -> Bool:
214 Pollrow.before.m(Pollrow.num.field("heldUntil", row), now)
215
216def expired_creds(+creds: String, +now: U32) -> Bool:
217 UCred.expired(UCred.oauth.of(creds), now)
218
219def expired.pick(on: Bool, +creds: String, +now: U32) -> Maybe<&2, String>:
220 match on:
221 case True{}:
222 Some{"token_expired"}
223 case False{}:
224 None{}
225
226
227
228def expired_sentinel(active: Bool, +row: Json.Json, +creds: String, +now: U32) -> Maybe<&2, String>:
229 expired.pick(active && Bool.not(held_row(row, now)) && expired_creds(creds, now), creds, now)
230
231def first(a: Maybe<&2, String>, b: Maybe<&2, String>) -> Maybe<&2, String>:
232 match a:
233 case Some{x}:
234 Some{x}
235 case None{}:
236 b
237
238def dead.pick(dead: Bool) -> Maybe<&2, String>:
239 match dead:
240 case True{}:
241 Some{"relogin_required"}
242 case False{}:
243 None{}
244
245def dead_sentinel(+d: U32) -> Maybe<&2, String>:
246 dead.pick(U32.is_eq(d, 1))
247
248
249type St is Data:
250 St{info: Info, row: Json.Json, sentinel: Maybe<&2, String>, healed: Bool}
251
252def st.mk(+i: Info, +row: Json.Json, +d: U32, +now: U32) -> St:
253 match i:
254 case Info{num, email, org_name, org_uuid, alias, disabled, active, creds}:
255 St{i, row, first(static_of(creds), first(dead_sentinel(d), expired_sentinel(active, row, creds, now))), U32.is_eq(d, 2)}
256
257def row_or_null(m: Maybe<&2, Json.Json>) -> Json.Json:
258 match m:
259 case Some{r}:
260 r
261 case None{}:
262 Json.of_pairs([])
263
264def creds_of(+i: Info) -> String:
265 match i:
266 case Info{num, email, org_name, org_uuid, alias, disabled, active, creds}:
267 creds
268
269def st.row(+i: Info, +row: Json.Json, +now: U32) -> IO(St):
270 IO.bind(U32, St, dead_state(row, creds_of(i)), d => IO.pure(St, st.mk(i, row, d, now)))
271
272def st.of(+i: Info, +rows: Json.Json, +now: U32) -> IO(St):
273 st.row(i, row_or_null(Ustore.row_of(rows, id_of(i))), now)
274
275def states(xs: List<&2, Info>, +rows: Json.Json, +now: U32) -> IO(List<&2, St>):
276 match xs:
277 case Nil{}:
278 IO.pure(List<&2, St>, [])
279 case Con{h, t}:
280 IO.bind(St, List<&2, St>, st.of(h, rows, now), s => IO.bind(List<&2, St>, List<&2, St>, states(t, rows, now), rest => IO.pure(List<&2, St>, s <> rest)))
281
282def st_slot(+s: St) -> String:
283 match s:
284 case St{info, row, sentinel, healed}:
285 slot_of(info)
286
287def st_healed(+s: St) -> Bool:
288 match s:
289 case St{info, row, sentinel, healed}:
290 healed
291
292def st_id(+s: St) -> Ustore.Id:
293 match s:
294 case St{info, row, sentinel, healed}:
295 id_of(info)
296
297
298def st_sentinel(+s: St) -> Maybe<&2, String>:
299 match s:
300 case St{info, row, sentinel, healed}:
301 sentinel