-
Notifications
You must be signed in to change notification settings - Fork 1
/
jail.go
149 lines (135 loc) · 4.65 KB
/
jail.go
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
138
139
140
141
142
143
144
145
146
147
148
149
package main
import (
"fmt"
"log"
"math/rand"
"time"
)
type JailResponse struct {
// Yes, "Requred", this is in their API. This key (like others) is typo'd.
// (Also typo'd in the inmate request, but NOT in <FACILITY>/NameSearch)
CaptchaRequired bool `json:"captchaRequred"`
// They'll keep updating this
CaptchaKey string `json:"captchaKey"`
// The initial list of inmate data
Offenders []Inmate `json:"offenders"`
// This is updated with every request
OffenderViewKey int `json:"offenderViewKey"`
// Empty string on success, non-empty on error.
// JailTracker sitll returns a 200 for what should be an internal server error or bad gateway,
// but this will at least be set.
ErrorMessage string `json:"errorMessage"`
}
// Jail is a top-level struct for a task to retrieve the list of inmates in a jail.
// This is also the type used to serialize to JSON for storage.
type Jail struct {
// BaseURL for the jail. Usually "https://omsweb.public-safety-cloud.com", but not always!
BaseURL string
// Name of the jail, as it appears in the URL
Name string
// This is sent with each request, and sometimes updated
CaptchaKey string
//TODO rename "offenders" to something more appropriate; this is JailTracker terminology
Offenders []Inmate
// Each request (after validation) updates this key!
OffenderViewKey int
// When the job started
StartTimeUTC time.Time
// When the job ended
EndTimeUTC time.Time
}
func NewJail(baseURL, name string) (*Jail, error) {
j := &Jail{
BaseURL: baseURL,
Name: name,
StartTimeUTC: time.Now().UTC(),
}
if err := j.updateCaptcha(); err != nil {
return nil, fmt.Errorf("failed to update captcha: %w", err)
}
log.Println("Captcha matched!")
// Make initial request for jail data
payload := &CaptchaProtocol{
CaptchaKey: j.CaptchaKey,
CaptchaImage: "",
// This is normally null in this request in the web client :\
UserCode: "",
}
jailResponse := &JailResponse{}
err := PostJSON[CaptchaProtocol, JailResponse](j.getJailAPIURL(), nil, payload, jailResponse)
if err != nil {
return nil, fmt.Errorf("failed to request initial jail data: %w", err)
}
if jailResponse.ErrorMessage != "" {
return nil, fmt.Errorf(`non-empty error message for jail "%s": "%s"`, name, jailResponse.ErrorMessage)
}
if jailResponse.CaptchaRequired {
return nil, fmt.Errorf("captcha required for jail. Response: %v", jailResponse)
}
j.OffenderViewKey = jailResponse.OffenderViewKey
j.Offenders = jailResponse.Offenders
return j, nil
}
func (j *Jail) updateCaptcha() error {
captchaMatched := false
var captchaKey string
var err error
for i := 0; i < MaxCaptchaAttempts; i++ {
captchaKey, err = ProcessCaptcha(j)
if err != nil {
log.Printf("failed to solve captcha: %v", err)
continue
}
captchaMatched = true
break
}
if !captchaMatched {
return fmt.Errorf("failed to match captcha after %d attempts", MaxCaptchaAttempts)
}
j.CaptchaKey = captchaKey
log.Println("Captcha matched!")
return nil
}
// UpdateInmates updates all inmates in the jail.
// Currently returns only a nil error, but reserving one here for future use.
func (j *Jail) UpdateInmates() error {
for i := range j.Offenders {
// Chill out for a bit to be especially gentle to their server
// Convert time.Second (duration in nanoseconds) to float, scale to 0.5-1.5 seconds
duration := time.Duration((0.5 + rand.Float64()) * float64(time.Second))
time.Sleep(duration)
inmate := &j.Offenders[i]
err := inmate.Update(j)
if err != nil {
log.Printf("failed to update inmate \"%s\": %v", inmate.ArrestNo, err)
continue
}
log.Printf("Updated inmate \"%s\". Cases: %d Charges: %d Holds: %d Booked: %s",
inmate.ArrestNo, len(inmate.Cases), len(inmate.Charges), len(inmate.Holds), inmate.OriginalBookDateTime,
)
}
return nil
}
// Get the URL for the jail's main page, as it would be accessed by a web browser.
// Jails have their own URL within the domain, but the captcha service needs to know which jail
// the captcha corresponds to, so it looks for this URL in the Referer header.
func (j Jail) getJailURL() string {
return fmt.Sprintf("%s/jtclientweb/jailtracker/index/%s", j.BaseURL, j.Name)
}
// Get the URL for the jail's JSON API, which will list all inmates.
func (j Jail) getJailAPIURL() string {
return fmt.Sprintf("%s/jtclientweb/Offender/%s", j.BaseURL, j.Name)
}
func CrawlJail(baseURL, name string) (*Jail, error) {
j, err := NewJail(baseURL, name)
if err != nil {
return nil, fmt.Errorf("failed to initialize jail: %w", err)
}
log.Printf("Found %d inmates", len(j.Offenders))
err = j.UpdateInmates()
if err != nil {
return nil, fmt.Errorf("failed to update inmates: %w", err)
}
j.EndTimeUTC = time.Now().UTC()
return j, nil
}