-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathmonitor.go
219 lines (192 loc) · 6.29 KB
/
monitor.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
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
package main
import (
"encoding/json"
"fmt"
"io"
"net/http"
"regexp"
"time"
c "github.com/ostafen/clover"
"github.com/rs/zerolog/log"
)
const (
HomeURL = "https://store.ui.com/us/en"
DiscordWebhookURL = "https://discord.com/api/webhooks/898013000000000000/3gWz4cG9xYh4gU7lHl3OcE5k8hBw0bZ4d4Y" // Replace with your Discord webhook URL
)
type UnifiStore struct {
ProdURLs []string
Headers map[string]string
}
type Product struct {
ID string `json:"id"`
Title string `json:"title"`
Status string `json:"status"`
ShortDescription string `json:"shortDescription"`
CollectionSlug string `json:"collectionSlug"`
Slug string `json:"slug"`
Thumbnail Thumbnail `json:"thumbnail"`
Variants []Variant `json:"variants"`
}
type Thumbnail struct {
URL string `json:"url"`
}
type Variant struct {
ID string `json:"id"`
DisplayPrice struct {
Amount int `json:"amount"`
Currency string `json:"currency"`
} `json:"displayPrice"`
}
type PageProps struct {
Product Product `json:"product"`
}
type Response struct {
PageProps PageProps `json:"pageProps"`
}
// NewUnifiStore initializes and returns a pointer to a new UnifiStore instance.
// It sets up default HTTP headers, including user-agent and cookie headers,
// necessary for making requests to the Unifi store. The Initialized field is
// set to false by default, indicating that the store has not yet loaded any
// product data.
func NewUnifiStore() *UnifiStore {
return &UnifiStore{
Headers: map[string]string{
"accept": "*/*",
"accept-language": "en-US,en;q=0.6",
"user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36",
"priority": "u=0, i",
},
}
}
// FetchXAppBuild fetches the x-app-build header from the Unifi store homepage.
//
// The x-app-build header is used to construct the URL for fetching product information.
// If the header is not found, an error is returned.
//
// FetchXAppBuild will set the ProdURL field of the UnifiStore object to the correct
// URL for fetching the product information.
func (store *UnifiStore) FetchXAppBuild() error {
logger.Info().Msg("Fetching X-App-Build...")
req, err := http.NewRequest("GET", HomeURL, nil)
if err != nil {
log.Error().Err(err).Msg("Failed to create request")
return err
}
for key, value := range store.Headers {
req.Header.Set(key, value)
}
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
log.Error().Err(err).Msg("Failed to Fetch Home Page")
return err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
logger.Error().Err(err).Msg("Failed to Read Response")
return err
}
soup := string(body)
regexPattern := `https://assets\.ecomm\.ui\.com/_next/static/([a-zA-Z0-9]+)/_buildManifest\.js`
re := regexp.MustCompile(regexPattern)
matches := re.FindStringSubmatch(soup)
if len(matches) > 1 {
buildID := matches[1]
store.ProdURLs = []string{
fmt.Sprintf("https://store.ui.com/_next/data/%s/us/en/category/all-power-tech/collections/power-tech/products/usp-pdu-pro.json", buildID),
fmt.Sprintf("https://store.ui.com/_next/data/%s/us/en/category/network-storage/collections/unifi-new-integrations-network-storage/products/unas-pro.json", buildID),
}
logger.Info().Msg(fmt.Sprintf("Extracted X-App-Build: %s", buildID))
return nil
}
//
logger.Error().Msg("X-App-Build Not Found")
return fmt.Errorf("X-App-Build Not Found")
}
// MonitorProduct fetches the product information from the Unifi store and
// returns the product or an error if the request fails or the JSON is
// malformed.
func (store *UnifiStore) MonitorProduct(url string) (Product, error) {
logger.Info().Msg("Monitoring Products")
req, err := http.NewRequest("GET", url, nil)
if err != nil {
log.Error().Err(err).Msg("Failed to create request")
return Product{}, err
}
for key, value := range store.Headers {
req.Header.Set(key, value)
}
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
log.Error().Err(err).Msg("Request Failed")
return Product{}, err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
logger.Error().Err(err).Msg("Failed Response Read")
return Product{}, err
}
var response Response
if err := json.Unmarshal(body, &response); err != nil {
return Product{}, fmt.Errorf("failed to parse JSON: %v", err)
}
logger.Info().Msg(fmt.Sprintf("Product: %+v", response.PageProps.Product.Title))
return response.PageProps.Product, nil
}
// Start runs an infinite loop where it fetches the X-App-Build and monitors
// the product. If either operation fails, it logs the error and waits 30
// seconds before trying again.
func (store *UnifiStore) Start() {
logger.Info().Msg("Starting Monitor")
var err error
db, err := c.Open("clover-db")
if err != nil {
logger.Fatal().Err(err).Msg("Failed to open database")
}
db.CreateCollection("products")
for {
if err := store.FetchXAppBuild(); err != nil {
logger.Error().Err(err).Msg("Failed to Fetch X-App-Build")
time.Sleep(30 * time.Second)
continue
}
for _, url := range store.ProdURLs {
product, err := store.MonitorProduct(url)
doc := c.NewDocument()
doc.Set("products", product)
docId, _ := db.InsertOne("products", doc)
logger.Info().Msg(fmt.Sprintf("Inserted Document: %s", docId))
db.ExportCollection("products", "products.json")
defer db.Close()
if err != nil {
logger.Error().Err(err).Msg("Failed to Monitor Product")
time.Sleep(30 * time.Second)
continue
}
// redditClient, err := CreateRedditClient()
// if err != nil {
// logger.Error().Err(err).Msg("Failed to create Reddit client")
// time.Sleep(30 * time.Second)
// continue
// }
if product.Status == "Available" {
err = SendWebhook(product)
if err != nil {
logger.Error().Err(err).Msg("Failed to send Discord webhook")
}
// err = CreatePost(redditClient, product)
// if err != nil {
// logger.Error().Err(err).Msg("Failed to create Reddit post")
// time.Sleep(30 * time.Second)
// continue
// }
}
}
logger.Info().Msg("Product Not InStock - Checking Again in 30s")
// logger.Info().Msg(fmt.Sprintf("Product: %+v", product))
time.Sleep(30 * time.Second)
}
}