Repository navigation
Expand file tree
/
Copy pathotp.go
More file actions
116 lines (108 loc) · 3.07 KB
/
Copy pathotp.go
File metadata and controls
116 lines (108 loc) · 3.07 KB
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
package main
import (
"bufio"
"bytes"
"context"
"encoding/json"
"fmt"
"os/exec"
"regexp"
"strings"
"time"
)
type smsMessage struct {
Text string `json:"text"`
Sender string `json:"sender"`
CreatedAt time.Time `json:"created_at"`
IsFromMe bool `json:"is_from_me"`
}
var phonePattern = regexp.MustCompile(`^[6-9][0-9]{9}$`)
var zeptoSender = regexp.MustCompile(`(?i)^(?:[A-Z]{2}-)?ZEPTOO(?:-[A-Z])?$`)
var blinkitSender = regexp.MustCompile(`(?i)^(?:[A-Z]{2}-)?BLNKIT(?:-[A-Z])?$`)
func normalizePhone(phone string) (string, error) {
phone = strings.TrimSpace(phone)
phone = strings.TrimPrefix(phone, "+91")
if !phonePattern.MatchString(phone) {
return "", fmt.Errorf("provide a 10-digit Indian mobile number, optionally prefixed with +91")
}
return phone, nil
}
func otpDigits(p Provider) int {
if p == Blinkit {
return 4
}
return 6
}
func extractOTP(p Provider, raw []byte, since, now time.Time) (string, error) {
scan := bufio.NewScanner(bytes.NewReader(raw))
scan.Buffer(make([]byte, 4096), 1024*1024)
codes := map[string]bool{}
codeRE := regexp.MustCompile(fmt.Sprintf(`\b[0-9]{%d}\b`, otpDigits(p)))
senderRE := zeptoSender
if p == Blinkit {
senderRE = blinkitSender
}
for scan.Scan() {
var m smsMessage
if json.Unmarshal(scan.Bytes(), &m) != nil {
return "", fmt.Errorf("invalid imsg response")
}
if m.IsFromMe || m.CreatedAt.Before(since) || m.CreatedAt.After(now) || !senderRE.MatchString(m.Sender) {
continue
}
text := strings.ToLower(m.Text)
if !strings.Contains(text, string(p)) || !(strings.Contains(text, "otp") || strings.Contains(text, "verification code")) {
continue
}
matches := codeRE.FindAllString(m.Text, -1)
unique := map[string]bool{}
for _, match := range matches {
unique[match] = true
}
if len(unique) != 1 {
continue
}
for match := range unique {
codes[match] = true
}
}
if err := scan.Err(); err != nil {
return "", fmt.Errorf("imsg response too large")
}
if len(codes) > 1 {
return "", fmt.Errorf("multiple fresh login codes found; retry with manual OTP input")
}
for code := range codes {
return code, nil
}
return "", nil
}
func readMessages(ctx context.Context, p Provider) ([]byte, error) {
ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel()
cmd := exec.CommandContext(ctx, "imsg", "search", "--query", string(p), "--limit", "20", "--json")
raw, err := cmd.Output()
if err != nil {
return nil, fmt.Errorf("imsg cannot read Messages; enable Full Disk Access for the host terminal/app and restart it")
}
return raw, nil
}
func waitOTP(ctx context.Context, p Provider, since time.Time) (string, error) {
deadline, cancel := context.WithTimeout(ctx, 90*time.Second)
defer cancel()
for {
raw, err := readMessages(deadline, p)
if err != nil {
return "", err
}
code, err := extractOTP(p, raw, since, time.Now())
if err != nil || code != "" {
return code, err
}
select {
case <-deadline.Done():
return "", fmt.Errorf("no fresh %s OTP received within 90 seconds; no resend attempted", p)
case <-time.After(2 * time.Second):
}
}
}