-
Notifications
You must be signed in to change notification settings - Fork 2
/
TooGoodToGo.scala
133 lines (120 loc) · 4.96 KB
/
TooGoodToGo.scala
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
package tgtg
import cats.effect.IO
import cats.effect.std.Random
import cats.syntax.all.*
import org.legogroup.woof.{Logger, given}
import sttp.client4.*
import sttp.client4.circe.*
import sttp.model.*
import tgtg.cache.{CacheKey, CacheService}
import java.util.concurrent.TimeUnit
import scala.concurrent.duration.*
class TooGoodToGo(http: Backend[IO])(using log: Logger[IO]):
private val baseUri = uri"https://apptoogoodtogo.com/api/"
private val refreshEndpoint = uri"${baseUri}auth/v4/token/refresh"
private val itemsEndpoint = uri"${baseUri}item/v8/"
private val loginEndpoint = uri"${baseUri}auth/v4/authByEmail"
private val authPollEndpoint = uri"${baseUri}auth/v4/authByRequestPollingId"
def getItems(cache: CacheService, config: TgtgConfig) =
def retrieveItems(access: AccessToken) =
headers()
.flatMap(baseHeaders =>
basicRequest
.body(asJson(GetItemsRequest()))
.post(itemsEndpoint)
.cookies(access.cookies.toSeq*)
.headers(baseHeaders*)
.auth
.bearer(access.access_token)
.response(asJsonOrFail[GetItemsResponse])
.send(http)
)
.map(_.body.items)
.flatTap(items => log.debug(show"Found ${items.length} stores."))
.logTimed("retrieve stores")
getAccessToken(cache, config).flatMap(retrieveItems)
end getItems
def getAccessToken(cache: CacheService, config: TgtgConfig): IO[AccessToken] =
val action = headers()
.flatMap: baseHeaders =>
basicRequest
.body(asJson(RefreshRequest(config.refreshToken)))
.post(refreshEndpoint)
.response(asJsonOrFail[RefreshResponse])
.headers(baseHeaders*)
.send(http)
.map(r =>
AccessToken(
r.cookies
.collect:
case Right(c) => c
.map(c => (c.name, c.value)),
r.body.access_token,
ttl = FiniteDuration(r.body.access_token_ttl_seconds, TimeUnit.SECONDS)
)
)
cache.retrieveOrSet(action, CacheKey(s"tgtg/accessToken"), a => a.ttl / 4 * 3)
end getAccessToken
def getCredentials(email: Email): IO[TgtgConfig] =
headers().flatMap: baseHeaders =>
basicRequest
.body(asJson(LoginRequest(email)))
.post(loginEndpoint)
.response(asJsonOrFail[LoginResponse])
.headers(baseHeaders*)
.send(http)
.flatMap: r =>
r.code match
case StatusCode.Ok if r.body.state == "TERMS" =>
IO.raiseError(
new Exception(
show"This email $email is not linked to a tgtg account. Please signup with this email first."
)
)
case StatusCode.Ok if r.body.state == "WAIT" && r.body.polling_id.isDefined =>
val maxPollRetries = 24
val pollSleep = 5.seconds
val pollId = r.body.polling_id.get
val pollRequest: IO[Option[PollResponse]] = basicRequest
.body(asJson(PollRequest(email, pollId)))
.post(authPollEndpoint)
.response(asJsonOrFail[Option[PollResponse]])
.headers(baseHeaders*)
.send(http)
.logTimed("poll")
.flatMap: response =>
if response.code == StatusCode.Accepted then IO.none
else response.body.pure
def poll(triesLeft: Int): IO[TgtgConfig] =
if triesLeft <= 0 then
IO.raiseError(new Exception(show"Max retries (${maxPollRetries * pollSleep}) reached. Try again."))
else
// Poll, or sleep and try again
pollRequest.flatMap {
case None => IO.sleep(pollSleep) >> poll(triesLeft - 1)
case Some(value) =>
TgtgConfig(refreshToken = value.refresh_token).pure
}
log.info(
"Check your mailbox on PC to continue... (Mailbox on mobile won't work, if you have installed tgtg app.)\n"
) *>
poll(maxPollRetries)
case StatusCode.TooManyRequests => IO.raiseError(new Exception("Too many requests. Try again later."))
case _: StatusCode => IO.raiseError(new Exception(show"Unexpected response: ${r.show()}"))
private def headers(): IO[Seq[Header]] = Random
.scalaUtilRandom[IO]
.flatMap(_.betweenInt(0, userAgents.length))
.map(i =>
Seq(
Header.accept(MediaType.ApplicationJson),
Header.acceptEncoding("gzip"),
Header(HeaderNames.AcceptLanguage, "en-GB"),
Header.userAgent(userAgents(i))
)
)
private def userAgents = List(
"TGTG/23.6.11 Dalvik/2.1.0 (Linux; U; Android 9; Nexus 5 Build/M4B30Z)",
"TGTG/23.6.11 Dalvik/2.1.0 (Linux; U; Android 10; SM-G935F Build/NRD90M)",
"TGTG/23.6.11 Dalvik/2.1.0 (Linux; Android 12; SM-G920V Build/MMB29K)"
)
end TooGoodToGo