1 import Base 2 import ./pollrow.bend as Pollrow 3 import ./usage/gate.bend as UGate 4 import ./store/seq.bend as SSeq 5 import ./json.bend as Json 6 import ./view.bend as View 7 import ./collect.bend as Collect 8 9 # The list payload's additive warnings (switcher.py _duplicate_account_warnings 10 # and _lockstep_usage_warnings). Credentials are compared by lineage identity 11 # as text (UGate.fp), so no hash is spawned. 12 13 def dash() -> String: 14 " \u{2014} " 15 16 type Seen is Data: 17 Seen{key: String, slot: String} 18 19 def seen.pick(hit: Bool, s: String, rest: Maybe<&2, String>) -> Maybe<&2, String>: 20 match hit: 21 case True{}: 22 Some{s} 23 case False{}: 24 rest 25 26 def seen_find(+key: String, xs: List<&2, Seen>) -> Maybe<&2, String>: 27 match xs: 28 case Nil{}: 29 None{} 30 case Con{Seen{k, s}, t}: 31 seen.pick(String.eq(k, key), s, seen_find(key, t)) 32 33 def append_str(xs: List<&2, String>, +x: String) -> List<&2, String>: 34 List.append(&2, String, xs, [x]) 35 36 def win_key2(p: Maybe<&2, Json.Json>, r: Maybe<&2, Json.Json>) -> Maybe<&2, String>: 37 match p r: 38 case Some{Json.JNum{t}} Some{Json.JStr{s}}: 39 Some{t ++ "@" ++ s} 40 case _ _: 41 None{} 42 43 def win_key(+w: Json.Json) -> Maybe<&2, String>: 44 win_key2(Json.get("pct", w), Json.get("resets_at", w)) 45 46 def key3(a: Maybe<&2, String>, b: Maybe<&2, String>) -> Maybe<&2, String>: 47 match a b: 48 case Some{x} Some{y}: 49 Some{x ++ "|" ++ y} 50 case _ _: 51 None{} 52 53 def key2(a: Maybe<&2, Json.Json>, b: Maybe<&2, Json.Json>) -> Maybe<&2, String>: 54 match a b: 55 case Some{x} Some{y}: 56 key3(win_key(x), win_key(y)) 57 case _ _: 58 None{} 59 60 def usage_key(+u: Json.Json) -> Maybe<&2, String>: 61 key2(Json.get("five_hour", u), Json.get("seven_day", u)) 62 63 type LS is Data: 64 LS{seen: List<&2, Seen>, msgs: List<&2, String>} 65 66 def ls_seen(ls: LS) -> List<&2, Seen>: 67 match ls: 68 case LS{s, m}: 69 s 70 71 def lock.seen(other: Maybe<&2, String>, +k: String, +slot: String, ls: LS) -> LS: 72 match other ls: 73 case Some{o} LS{s, msgs}: 74 LS{s, append_str(msgs, "Account-" ++ o ++ " and Account-" ++ slot ++ " report identical usage and reset times" ++ dash() ++ "they may be the same account (issue #117). If it persists, log in with the missing account and re-add it: cswap add --slot N")} 75 case None{} LS{s, msgs}: 76 LS{Seen{k, slot} <> s, msgs} 77 78 def lock.step(m: Maybe<&2, String>, +slot: String, +ls: LS) -> LS: 79 match m: 80 case None{}: 81 ls 82 case Some{+k}: 83 lock.seen(seen_find(k, ls_seen(ls)), k, slot, ls) 84 85 def lock.live(sentinel: Maybe<&2, String>, +row: Pollrow.Row, +slot: String, models: List<&2, String>, +now: U32, ls: LS) -> LS: 86 match sentinel: 87 case Some{s}: 88 ls 89 case None{}: 90 lock.step(usage_key(Pollrow.decision_usage(row, models, now)), slot, ls) 91 92 def lock.acct(+a: View.Acct, models: List<&2, String>, +now: U32, ls: LS) -> LS: 93 match a: 94 case View.Acct{num, email, org_name, org_uuid, active, alias, disabled, login, sentinel, row}: 95 lock.live(sentinel, row, U32.show(num), models, now, ls) 96 97 def lock.go(as: List<&2, View.Acct>, +models: List<&2, String>, +now: U32, ls: LS) -> LS: 98 match as: 99 case Nil{}: 100 ls 101 case Con{h, t}: 102 lock.go(t, models, now, lock.acct(h, models, now, ls)) 103 104 def msgs_of(ls: LS) -> List<&2, String>: 105 match ls: 106 case LS{s, m}: 107 m 108 109 # Slots whose 5h and 7d usage and resets match to the digit. 110 def lockstep(as: List<&2, View.Acct>, models: List<&2, String>, +now: U32) -> List<&2, String>: 111 msgs_of(lock.go(as, models, now, LS{[], []})) 112 113 type DS is Data: 114 DS{fps: List<&2, Seen>, ids: List<&2, Seen>, msgs: List<&2, String>} 115 116 def uuid_of(+seq: Json.Json, +slot: String) -> String: 117 String.trim(Maybe.default(&2, String, Json.as_string(Maybe.default(&2, Json.Json, Maybe.bind(&2, Json.Json, Json.Json, SSeq.slot_record(seq, slot), r => Json.get("uuid", r)), Json.JNull{})), "")) 118 119 def ds_fps(ds: DS) -> List<&2, Seen>: 120 match ds: 121 case DS{f, i, m}: 122 f 123 124 def dup.fp3(other: Maybe<&2, String>, +fp: String, +slot: String, +email: String, ds: DS) -> DS: 125 match other ds: 126 case Some{o} DS{f, i, m}: 127 DS{f, i, append_str(m, "Account-" ++ o ++ " and Account-" ++ slot ++ " hold the same credential (" ++ email ++ ")" ++ dash() ++ "one slot's backup was overwritten. Log in with the missing account and re-add it: cswap add --slot N")} 128 case None{} DS{f, i, m}: 129 DS{Seen{fp, slot} <> f, i, m} 130 131 def dup.fp2(has: Bool, +fp: String, +slot: String, +email: String, +ds: DS) -> DS: 132 match has: 133 case False{}: 134 ds 135 case True{}: 136 dup.fp3(seen_find(fp, ds_fps(ds)), fp, slot, email, ds) 137 138 def dup.fp(+creds: String, +slot: String, +email: String, ds: DS) -> DS: 139 dup.fp2(Bool.not(String.is_empty(creds)), UGate.fp(creds), slot, email, ds) 140 141 def ds_ids(ds: DS) -> List<&2, Seen>: 142 match ds: 143 case DS{f, i, m}: 144 i 145 146 def dup.id3(other: Maybe<&2, String>, +key: String, +slot: String, +email: String, ds: DS) -> DS: 147 match other ds: 148 case Some{o} DS{f, i, m}: 149 DS{f, i, append_str(m, "Account-" ++ o ++ " and Account-" ++ slot ++ " both authenticate as " ++ email ++ dash() ++ "remove or re-login one of them.")} 150 case None{} DS{f, i, m}: 151 DS{f, Seen{key, slot} <> i, m} 152 153 def dup.id2(has: Bool, +key: String, +slot: String, +email: String, +ds: DS) -> DS: 154 match has: 155 case False{}: 156 ds 157 case True{}: 158 dup.id3(seen_find(key, ds_ids(ds)), key, slot, email, ds) 159 160 def dup.id(+uuid: String, +org: String, +slot: String, +email: String, ds: DS) -> DS: 161 dup.id2(Bool.not(String.is_empty(uuid)), uuid ++ "@" ++ org, slot, email, ds) 162 163 def dup.info(+i: Collect.Info, +seq: Json.Json, ds: DS) -> DS: 164 match i: 165 case Collect.Info{num, email, org_name, org_uuid, alias, disabled, active, creds}: 166 dup.id(uuid_of(seq, U32.show(num)), org_uuid, U32.show(num), email, dup.fp(creds, U32.show(num), email, ds)) 167 168 def dup.go(xs: List<&2, Collect.Info>, +seq: Json.Json, ds: DS) -> DS: 169 match xs: 170 case Nil{}: 171 ds 172 case Con{h, t}: 173 dup.go(t, seq, dup.info(h, seq, ds)) 174 175 def ds_msgs(ds: DS) -> List<&2, String>: 176 match ds: 177 case DS{f, i, m}: 178 m 179 180 # Slots that provably authenticate as the same account. 181 def duplicates(infos: List<&2, Collect.Info>, +seq: Json.Json) -> List<&2, String>: 182 ds_msgs(dup.go(infos, seq, DS{[], [], []}))