alby_service.go raw
1 package alby
2
3 import (
4 "context"
5 "encoding/json"
6 "errors"
7 "fmt"
8 "io"
9 "net/http"
10 "strconv"
11 "strings"
12 "time"
13
14 "github.com/getAlby/hub/logger"
15 "github.com/sirupsen/logrus"
16 )
17
18 const albyInternalAPIURL = "https://getalby.com/api"
19
20 // hubInfoURL is the update-check source for this fork. It points at the fork's
21 // own git repo tags so an "update available" banner only ever appears for
22 // versions this fork has actually published, never for upstream Alby releases.
23 const hubInfoURL = "https://git.smesh.lol/hvat/info/refs?service=git-upload-pack"
24
25 type albyService struct {
26 }
27
28 func NewAlbyService() *albyService {
29 return &albyService{}
30 }
31
32 func (svc *albyService) GetCurrencies(ctx context.Context) ([]Currency, error) {
33 client := &http.Client{Timeout: 10 * time.Second}
34 url := fmt.Sprintf("%s/rates", albyInternalAPIURL)
35
36 req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
37 if err != nil {
38 logger.Logger.WithError(err).Error("Error creating request to currencies endpoint")
39 return nil, err
40 }
41 setDefaultRequestHeaders(req)
42
43 res, err := client.Do(req)
44 if err != nil {
45 logger.Logger.WithError(err).Error("Failed to fetch currencies from API")
46 return nil, err
47 }
48
49 defer res.Body.Close()
50
51 body, err := io.ReadAll(res.Body)
52 if err != nil {
53 logger.Logger.WithError(err).WithFields(logrus.Fields{
54 "url": url,
55 }).Error("Failed to read response body")
56 return nil, errors.New("failed to read response body")
57 }
58
59 if res.StatusCode >= 300 {
60 logger.Logger.WithFields(logrus.Fields{
61 "body": string(body),
62 "status_code": res.StatusCode,
63 }).Error("Currencies endpoint returned non-success code")
64 return nil, fmt.Errorf("currencies endpoint returned non-success code: %s", string(body))
65 }
66
67 rawCurrencies := map[string]Currency{}
68 err = json.Unmarshal(body, &rawCurrencies)
69 if err != nil {
70 logger.Logger.WithFields(logrus.Fields{
71 "body": string(body),
72 "error": err,
73 }).Error("Failed to decode currencies API response")
74 return nil, err
75 }
76
77 currencies := []Currency{}
78 for _, currency := range rawCurrencies {
79 currencies = append(currencies, currency)
80 }
81
82 return currencies, nil
83 }
84
85 func (svc *albyService) GetBitcoinRate(ctx context.Context, currency string) (*BitcoinRate, error) {
86 client := &http.Client{Timeout: 10 * time.Second}
87
88 url := fmt.Sprintf("%s/rates/%s", albyInternalAPIURL, currency)
89
90 req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
91 if err != nil {
92 logger.Logger.WithFields(logrus.Fields{
93 "currency": currency,
94 "error": err,
95 }).Error("Error creating request to Bitcoin rate endpoint")
96 return nil, err
97 }
98 setDefaultRequestHeaders(req)
99
100 res, err := client.Do(req)
101 if err != nil {
102 logger.Logger.WithFields(logrus.Fields{
103 "currency": currency,
104 "error": err,
105 }).Error("Failed to fetch Bitcoin rate from API")
106 return nil, err
107 }
108
109 defer res.Body.Close()
110
111 body, err := io.ReadAll(res.Body)
112 if err != nil {
113 logger.Logger.WithError(err).WithFields(logrus.Fields{
114 "url": url,
115 }).Error("Failed to read response body")
116 return nil, errors.New("failed to read response body")
117 }
118
119 if res.StatusCode >= 300 {
120 logger.Logger.WithFields(logrus.Fields{
121 "currency": currency,
122 "body": string(body),
123 "status_code": res.StatusCode,
124 }).Error("Bitcoin rate endpoint returned non-success code")
125 return nil, fmt.Errorf("bitcoin rate endpoint returned non-success code: %s", string(body))
126 }
127
128 var rate = &BitcoinRate{}
129 err = json.Unmarshal(body, rate)
130 if err != nil {
131 logger.Logger.WithFields(logrus.Fields{
132 "currency": currency,
133 "body": string(body),
134 "error": err,
135 }).Error("Failed to decode Bitcoin rate API response")
136 return nil, err
137 }
138
139 return rate, nil
140 }
141
142 func (svc *albyService) GetChannelPeerSuggestions(ctx context.Context) ([]ChannelPeerSuggestion, error) {
143 client := &http.Client{Timeout: 10 * time.Second}
144
145 req, err := http.NewRequestWithContext(ctx, "GET", fmt.Sprintf("%s/internal/channel_suggestions", albyInternalAPIURL), nil)
146 if err != nil {
147 logger.Logger.WithError(err).Error("Error creating request to channel_suggestions endpoint")
148 return nil, err
149 }
150
151 setDefaultRequestHeaders(req)
152
153 res, err := client.Do(req)
154 if err != nil {
155 logger.Logger.WithError(err).Error("Failed to fetch channel_suggestions endpoint")
156 return nil, err
157 }
158
159 body, err := io.ReadAll(res.Body)
160 if err != nil {
161 logger.Logger.WithError(err).Error("Failed to read response body")
162 return nil, errors.New("failed to read response body")
163 }
164
165 if res.StatusCode >= 300 {
166 logger.Logger.WithFields(logrus.Fields{
167 "body": string(body),
168 "status_code": res.StatusCode,
169 }).Error("channel suggestions endpoint returned non-success code")
170 return nil, fmt.Errorf("channel suggestions endpoint returned non-success code: %s", string(body))
171 }
172
173 var suggestions []ChannelPeerSuggestion
174 err = json.Unmarshal(body, &suggestions)
175 if err != nil {
176 logger.Logger.WithError(err).Errorf("Failed to decode API response")
177 return nil, err
178 }
179
180 for i := range suggestions {
181 suggestions[i].MinimumChannelSizeSat = suggestions[i].MinimumChannelSize
182 suggestions[i].MaximumChannelSizeSat = suggestions[i].MaximumChannelSize
183 }
184
185 logger.Logger.WithFields(logrus.Fields{"channel_suggestions": suggestions}).Debug("Alby channel peer suggestions response")
186 return suggestions, nil
187 }
188
189 func (svc *albyService) GetInfo(ctx context.Context) (*AlbyInfo, error) {
190 client := &http.Client{Timeout: 10 * time.Second}
191
192 req, err := http.NewRequestWithContext(ctx, "GET", hubInfoURL, nil)
193 if err != nil {
194 logger.Logger.WithError(err).Error("Error creating request to hub info endpoint")
195 return nil, err
196 }
197
198 setDefaultRequestHeaders(req)
199
200 res, err := client.Do(req)
201 if err != nil {
202 logger.Logger.WithError(err).Error("Failed to fetch hub info")
203 return nil, err
204 }
205 defer res.Body.Close()
206
207 body, err := io.ReadAll(res.Body)
208 if err != nil {
209 logger.Logger.WithError(err).Error("Failed to read response body")
210 return nil, errors.New("failed to read response body")
211 }
212
213 if res.StatusCode >= 300 {
214 logger.Logger.WithFields(logrus.Fields{
215 "status_code": res.StatusCode,
216 }).Error("hub info endpoint returned non-success code")
217 return nil, fmt.Errorf("hub info endpoint returned non-success code: %d", res.StatusCode)
218 }
219
220 latestTag := parseLatestVersionTag(string(body))
221
222 // default to 0.0.0 so an un-tagged repo never triggers an update banner
223 latestVersion := "0.0.0"
224 if latestTag != "" {
225 latestVersion = strings.TrimPrefix(latestTag, "v")
226 }
227
228 return &AlbyInfo{
229 Hub: AlbyInfoHub{
230 LatestVersion: latestVersion,
231 LatestReleaseNotes: latestTag,
232 },
233 }, nil
234 }
235
236 // parseLatestVersionTag extracts the highest "vX.Y.Z" tag from git smart-HTTP
237 // info/refs output (pkt-line encoded).
238 func parseLatestVersionTag(refs string) string {
239 var latest string
240 for _, line := range strings.Split(refs, "\n") {
241 line = strings.TrimRight(line, "\r")
242 const marker = "refs/tags/"
243 idx := strings.Index(line, marker)
244 if idx < 0 {
245 continue
246 }
247 tag := strings.TrimSpace(line[idx+len(marker):])
248 if tag == "" || strings.HasSuffix(tag, "^{}") || !strings.HasPrefix(tag, "v") {
249 continue
250 }
251 if latest == "" || compareVersionTags(tag, latest) > 0 {
252 latest = tag
253 }
254 }
255 return latest
256 }
257
258 func compareVersionTags(a, b string) int {
259 na := strings.SplitN(strings.TrimPrefix(a, "v"), "-", 2)
260 nb := strings.SplitN(strings.TrimPrefix(b, "v"), "-", 2)
261 pa := strings.Split(na[0], ".")
262 pb := strings.Split(nb[0], ".")
263 for i := 0; i < 3; i++ {
264 va, vb := 0, 0
265 if i < len(pa) {
266 va, _ = strconv.Atoi(pa[i])
267 }
268 if i < len(pb) {
269 vb, _ = strconv.Atoi(pb[i])
270 }
271 if va < vb {
272 return -1
273 }
274 if va > vb {
275 return 1
276 }
277 }
278 return 0
279 }
280