1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
package appcli
import (
"bufio"
"cmp"
"context"
"fmt"
"io"
"strings"
qrcode "github.com/skip2/go-qrcode"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"github.com/johnandersen777/socialweb-computer-kcp/api/v1alpha1"
)
type MarketIdentityClient interface {
GetMarketIdentity(ctx context.Context, name string) (*v1alpha1.MarketIdentity, error)
ApproveAssociation(ctx context.Context, name, callerDid string) error
RejectAssociation(ctx context.Context, name, callerDid string) error
}
type MarketIdentityWatch func(ctx context.Context, name string, each func(*v1alpha1.MarketIdentity) (bool, error)) (*v1alpha1.MarketIdentity, error)
type IdentityRenderer struct {
Client MarketIdentityClient
Watch MarketIdentityWatch
Out io.Writer
In io.Reader
Hint func(*v1alpha1.MarketIdentity) string
}
func actionKey(a *v1alpha1.MarketIdentityPendingAction) string {
if a == nil {
return ""
}
return strings.Join([]string{string(a.Kind), a.URL, a.CallerDid}, "|")
}
func (r IdentityRenderer) WaitReady(ctx context.Context, name string) (*v1alpha1.MarketIdentity, error) {
if r.Watch == nil {
return nil, fmt.Errorf("marketidentity %s: the renderer has no watch", name)
}
var in *bufio.Reader
if r.In != nil {
in = bufio.NewReader(r.In)
}
shown, status := "", ""
var failed error
obj, err := r.Watch(ctx, name, func(obj *v1alpha1.MarketIdentity) (bool, error) {
if obj == nil {
return false, fmt.Errorf("marketidentity %s: kcpclient: read marketidentity %s: %w", name, name,
apierrors.NewNotFound(v1alpha1.MarketIdentityGVR.GroupResource(), name))
}
switch obj.Status.Phase {
case v1alpha1.MarketIdentityPhaseReady:
fmt.Fprintf(r.Out, "signed in as %s (%s)\n", cmp.Or(obj.Status.Handle), cmp.Or(obj.Status.Did))
return true, nil
case v1alpha1.MarketIdentityPhaseFailed:
failed = fmt.Errorf("marketidentity %s failed: %s", name, obj.Status.Message)
return true, nil
}
if st := string(obj.Status.Phase) + "|" + obj.Status.Did + "|" + obj.Status.Message; obj.Status.Phase != "" && st != status {
status = st
fmt.Fprintf(r.Out, "identity %s is %s (%s): %s\n", name, obj.Status.Phase, cmp.Or(obj.Status.Did, "no DID yet"), obj.Status.Message)
if r.Hint != nil {
if hint := r.Hint(obj); hint != "" {
fmt.Fprintln(r.Out, hint)
}
}
}
if key := actionKey(obj.Status.PendingAction); key != "" && key != shown {
shown = key
if err := r.render(ctx, name, obj.Status.PendingAction, in); err != nil {
return false, err
}
}
return false, nil
})
if err != nil {
if obj != nil && obj.Status.Phase != "" {
return obj, fmt.Errorf("marketidentity %s is %s, not Ready (%s): %w", name, obj.Status.Phase, obj.Status.Message, err)
}
return obj, err
}
return obj, failed
}
func (r IdentityRenderer) render(ctx context.Context, name string, a *v1alpha1.MarketIdentityPendingAction, in *bufio.Reader) error {
switch a.Kind {
case v1alpha1.PendingActionScanQr:
r.showUrl(name, "Scan this QR code with your phone to sign in:", a)
case v1alpha1.PendingActionVisitUrl:
r.showUrl(name, "Open this URL in a browser to sign in:", a)
case v1alpha1.PendingActionSignInAgain:
r.showUrl(name, "The sign-in session expired or lacks new scopes. Scan again to sign in:", a)
case v1alpha1.PendingActionApproveAssociation:
who := a.CallerDid
if a.Handle != "" && a.Handle != a.CallerDid {
who = fmt.Sprintf("%s (%s)", a.Handle, a.CallerDid)
}
if in == nil {
fmt.Fprintf(r.Out, "%s asks to associate; the accepted path is the operator's verified badgeBlueKeys claim naming %s with the role service; adding the caller to spec.approvedAssociations is a manual workaround only\n", who, a.CallerDid)
return nil
}
fmt.Fprintf(r.Out, "Associate with %s? [y/N] ", who)
line, err := in.ReadString('\n')
if err != nil && err != io.EOF {
return err
}
if strings.HasPrefix(strings.ToLower(strings.TrimSpace(line)), "y") {
return r.Client.ApproveAssociation(ctx, name, a.CallerDid)
}
return r.Client.RejectAssociation(ctx, name, a.CallerDid)
default:
fmt.Fprintf(r.Out, "marketidentity %s waits on %s\n", name, a.Kind)
}
return nil
}
func (r IdentityRenderer) showUrl(name, title string, a *v1alpha1.MarketIdentityPendingAction) {
rule := strings.Repeat("=", 60)
fmt.Fprintf(r.Out, "\n%s\n %s\n\n", rule, title)
if a.URL != "" {
if q, err := qrcode.New(a.URL, qrcode.High); err == nil {
fmt.Fprint(r.Out, q.ToSmallString(false))
}
fmt.Fprintf(r.Out, "\n Or open this URL:\n %s\n", a.URL)
}
if a.ExpiresAt != "" {
fmt.Fprintf(r.Out, " Expires at %s\n", a.ExpiresAt)
}
fmt.Fprintf(r.Out, "%s\n\n", rule)
if a.URL != "" {
fmt.Fprintf(r.Out, "identity %s pendingAction %s url %s expiresAt %s\n", name, a.Kind, a.URL, cmp.Or(a.ExpiresAt, "none"))
}
}