Skip to content

Commit 46729ec

Browse files
committed
reimplement anibis
1 parent d17b204 commit 46729ec

3 files changed

Lines changed: 363 additions & 1 deletion

File tree

lib/chuko/api/anibis.ex

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
defmodule Chuko.Api.Anibis do
2+
@moduledoc """
3+
Deprecated in favor of AnibisGql
4+
"""
25
@behaviour Chuko.Api.Platform
36
require Logger
47

lib/chuko/api/anibis_gql.ex

Lines changed: 359 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,359 @@
1+
defmodule Chuko.Api.AnibisGql do
2+
@moduledoc """
3+
Literal copy of the Tutti GraphQL API. Should combine them at some point.
4+
"""
5+
@behaviour Chuko.Api.Platform
6+
require Logger
7+
8+
@query """
9+
query SearchListings($query: String, $constraints: ListingSearchConstraints, $category: ID, $first: Int!, $offset: Int!, $sort: ListingSortMode!, $direction: SortDirection!) {
10+
searchListingsByQuery(
11+
query: $query
12+
constraints: $constraints
13+
category: $category
14+
) {
15+
...searchResultFields
16+
}
17+
}
18+
19+
fragment searchResultFields on ListingSearchResult {
20+
listings(first: $first, offset: $offset, sort: $sort, direction: $direction) {
21+
...listingsConnectionField
22+
}
23+
filters {
24+
...filterFields
25+
}
26+
suggestedCategories {
27+
...suggestedCategoryFields
28+
}
29+
selectedCategory {
30+
...selectedCategoryFields
31+
}
32+
}
33+
34+
fragment listingsConnectionField on ListingsConnection {
35+
totalCount
36+
edges {
37+
node {
38+
...listingFields
39+
}
40+
}
41+
placements {
42+
keyValues {
43+
key
44+
value
45+
}
46+
pageName
47+
pagePath
48+
positions {
49+
adUnitID
50+
mobile
51+
position
52+
positionType
53+
}
54+
afs {
55+
customChannelID
56+
styleID
57+
adUnits {
58+
adUnitID
59+
mobile
60+
}
61+
}
62+
}
63+
}
64+
65+
fragment listingFields on Listing {
66+
listingID
67+
title
68+
body
69+
postcodeInformation {
70+
postcode
71+
locationName
72+
canton {
73+
shortName
74+
name
75+
}
76+
}
77+
timestamp
78+
formattedPrice
79+
formattedSource
80+
highlighted
81+
sellerInfo {
82+
alias
83+
logo {
84+
rendition {
85+
src
86+
}
87+
}
88+
}
89+
images(first: 15) {
90+
rendition {
91+
src
92+
}
93+
}
94+
thumbnail {
95+
normalRendition: rendition(width: 235, height: 167) {
96+
src
97+
}
98+
retinaRendition: rendition(width: 470, height: 334) {
99+
src
100+
}
101+
}
102+
seoInformation {
103+
deSlug: slug(language: DE)
104+
frSlug: slug(language: FR)
105+
itSlug: slug(language: IT)
106+
}
107+
}
108+
109+
fragment filterFields on ListingFilter {
110+
__typename
111+
...nonGroupFilterFields
112+
}
113+
114+
fragment nonGroupFilterFields on ListingFilter {
115+
...filterDescriptionFields
116+
... on ListingIntervalFilter {
117+
...intervalFilterFields
118+
}
119+
... on ListingSingleSelectFilter {
120+
...singleSelectFilterFields
121+
}
122+
... on ListingMultiSelectFilter {
123+
...multiSelectFilterFields
124+
}
125+
... on ListingPricingFilter {
126+
...pricingFilterFields
127+
}
128+
... on ListingLocationFilter {
129+
...locationFilterFields
130+
}
131+
}
132+
133+
fragment filterDescriptionFields on ListingsFilterDescription {
134+
name
135+
label
136+
disabled
137+
}
138+
139+
fragment intervalFilterFields on ListingIntervalFilter {
140+
...filterDescriptionFields
141+
intervalType {
142+
__typename
143+
... on ListingIntervalTypeText {
144+
...intervalTypeTextFields
145+
}
146+
... on ListingIntervalTypeSlider {
147+
...intervalTypeSliderFields
148+
}
149+
}
150+
intervalValue: value {
151+
min
152+
max
153+
}
154+
step
155+
unit
156+
minField {
157+
placeholder
158+
}
159+
maxField {
160+
placeholder
161+
}
162+
}
163+
164+
fragment intervalTypeTextFields on ListingIntervalTypeText {
165+
minLimit
166+
maxLimit
167+
}
168+
169+
fragment intervalTypeSliderFields on ListingIntervalTypeSlider {
170+
sliderStart: minLimit
171+
sliderEnd: maxLimit
172+
}
173+
174+
fragment singleSelectFilterFields on ListingSingleSelectFilter {
175+
...filterDescriptionFields
176+
...selectFilterFields
177+
selectedOption: value
178+
}
179+
180+
fragment selectFilterFields on ListingSelectFilter {
181+
options {
182+
...selectOptionFields
183+
}
184+
placeholder
185+
inline
186+
}
187+
188+
fragment selectOptionFields on ListingSelectOption {
189+
value
190+
label
191+
}
192+
193+
fragment multiSelectFilterFields on ListingMultiSelectFilter {
194+
...filterDescriptionFields
195+
...selectFilterFields
196+
selectedOptions: values
197+
}
198+
199+
fragment pricingFilterFields on ListingPricingFilter {
200+
...filterDescriptionFields
201+
pricingValue: value {
202+
min
203+
max
204+
freeOnly
205+
}
206+
minField {
207+
placeholder
208+
}
209+
maxField {
210+
placeholder
211+
}
212+
}
213+
214+
fragment locationFilterFields on ListingLocationFilter {
215+
...filterDescriptionFields
216+
value {
217+
radius
218+
selectedLocalities {
219+
...localityFields
220+
}
221+
}
222+
}
223+
224+
fragment localityFields on Locality {
225+
localityID
226+
name
227+
localityType
228+
}
229+
230+
fragment suggestedCategoryFields on Category {
231+
categoryID
232+
label
233+
searchToken
234+
mainImage {
235+
rendition(width: 300) {
236+
src
237+
}
238+
}
239+
}
240+
241+
fragment selectedCategoryFields on Category {
242+
categoryID
243+
label
244+
...categoryParent
245+
}
246+
247+
fragment categoryParent on Category {
248+
parent {
249+
categoryID
250+
label
251+
parent {
252+
categoryID
253+
label
254+
parent {
255+
categoryID
256+
label
257+
}
258+
}
259+
}
260+
}
261+
"""
262+
263+
@url_item "https://www.anibis.ch/de/vi/"
264+
@url_api "https://www.anibis.ch/api/v10/graphql"
265+
266+
alias Chuko.Structs.Item
267+
268+
@impl true
269+
def search(query, session_id) when is_binary(query) do
270+
options = [
271+
json: %{
272+
query: @query,
273+
variables: %{
274+
query: query,
275+
constraints: nil,
276+
category: nil,
277+
first: 100,
278+
# max 3000
279+
offset: 0,
280+
direction: "DESCENDING",
281+
sort: "TIMESTAMP"
282+
}
283+
},
284+
headers: [
285+
user_agent: Chuko.AgentUser.get(),
286+
"Content-Type": "application/json",
287+
"x-tutti-client-identifier": "web/1.0.0+env-live.git-a70218e",
288+
"x-tutti-hash": Ecto.UUID.generate()
289+
],
290+
max_retries: 2
291+
]
292+
293+
# could be optimized by using the body
294+
amount =
295+
Req.post!(@url_api, put_in(options[:json][:variables][:first], 1))
296+
|> then(fn %Req.Response{body: body} ->
297+
body["data"]["searchListingsByQuery"]["listings"]["totalCount"]
298+
end)
299+
# Max is 3000 but capping at 500 for rate limit reasons
300+
|> then(&if &1 > 500, do: 500, else: &1)
301+
302+
0..floor(amount / 100)
303+
|> Enum.map(&(&1 * 100))
304+
|> Task.async_stream(
305+
fn offset ->
306+
Req.post!(@url_api, put_in(options[:json][:variables][:offset], offset))
307+
|> then(fn %Req.Response{body: body} ->
308+
body["data"]["searchListingsByQuery"]["listings"]["edges"]
309+
end)
310+
end,
311+
timeout: 30_000
312+
)
313+
|> Stream.flat_map(fn {:ok, res} -> List.wrap(res) end)
314+
|> Enum.filter(&(not is_nil(&1)))
315+
|> Enum.map(&cast_item(&1["node"]))
316+
rescue
317+
err ->
318+
Logger.error(Exception.format(:error, err, __STACKTRACE__))
319+
320+
Phoenix.PubSub.broadcast!(
321+
Chuko.PubSub,
322+
session_id,
323+
{:search_failed, %{platform: "Anibis"}}
324+
)
325+
326+
[]
327+
end
328+
329+
defp cast_item(json) do
330+
%Item{
331+
id: "anibis-#{json["listingID"]}",
332+
name: json["title"],
333+
description: json["body"],
334+
currency: "CHF",
335+
price: parse_price(json["formattedPrice"]),
336+
offer_type: :buynow,
337+
images: parse_images(json["images"]),
338+
url: "#{@url_item}#{json["seoInformation"]["deSlug"]}/#{json["listingID"]}",
339+
location: json["postcodeInformation"]["canton"]["name"],
340+
platform: :anibis,
341+
platform_logo: "/images/anibis_logo.png",
342+
created_at: DateTime.from_iso8601(json["timestamp"]) |> then(fn {:ok, dt, _} -> dt end)
343+
}
344+
end
345+
346+
defp parse_price(string) do
347+
string
348+
|> String.trim()
349+
|> String.replace("'", "")
350+
|> Integer.parse()
351+
|> case do
352+
{price, _} -> price / 1
353+
:error -> 0.0
354+
end
355+
end
356+
357+
defp parse_images(images) when is_list(images), do: Enum.map(images, & &1["rendition"]["src"])
358+
defp parse_images(_), do: []
359+
end

lib/chuko/search_engine.ex

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
defmodule Chuko.SearchEngine do
22
alias Chuko.Api.Platform
33

4-
@platforms [TuttiGql, Ricardo]
4+
@platforms [AnibisGql, TuttiGql, Ricardo]
55

66
def search_platforms(query, session_id, platforms \\ @platforms)
77
when is_binary(query) and is_list(platforms) do

0 commit comments

Comments
 (0)