-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #6 from Ulas-Scan/1-tokopedia-get-reviews
feat: get product id, get reviews
- Loading branch information
Showing
8 changed files
with
305 additions
and
5 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,80 @@ | ||
package controller | ||
|
||
import ( | ||
"net/http" | ||
"net/url" | ||
"strings" | ||
|
||
"ulascan-be/dto" | ||
"ulascan-be/service" | ||
"ulascan-be/utils" | ||
|
||
"github.com/gin-gonic/gin" | ||
) | ||
|
||
type ( | ||
TokopediaController interface { | ||
GetReviews(ctx *gin.Context) | ||
} | ||
|
||
tokopediaController struct { | ||
tokopediaService service.TokopediaService | ||
} | ||
) | ||
|
||
func NewTokopediaController(ts service.TokopediaService) TokopediaController { | ||
return &tokopediaController{ | ||
tokopediaService: ts, | ||
} | ||
} | ||
|
||
func (c *tokopediaController) GetReviews(ctx *gin.Context) { | ||
productUrl := ctx.Query("product_url") | ||
if productUrl == "" { | ||
res := utils.BuildResponseFailed(dto.MESSAGE_FAILED_GET_REVIEWS, dto.ErrProductUrlMissing.Error(), nil) | ||
ctx.AbortWithStatusJSON(http.StatusBadRequest, res) | ||
return | ||
} | ||
|
||
parsedUrl, err := url.Parse(productUrl) | ||
if err != nil { | ||
res := utils.BuildResponseFailed(dto.MESSAGE_FAILED_GET_REVIEWS, err.Error(), nil) | ||
ctx.AbortWithStatusJSON(http.StatusBadRequest, res) | ||
return | ||
} | ||
|
||
pathParts := strings.Split(parsedUrl.Path, "/") | ||
if len(pathParts) < 3 { | ||
res := utils.BuildResponseFailed(dto.MESSAGE_FAILED_GET_REVIEWS, dto.ErrProductUrlWrongFormat.Error(), nil) | ||
ctx.AbortWithStatusJSON(http.StatusBadRequest, res) | ||
return | ||
} | ||
|
||
productReq := dto.GetProductIdRequest{ | ||
ShopDomain: pathParts[1], | ||
ProductKey: pathParts[2], | ||
ProductUrl: "https://www.tokopedia.com/" + pathParts[1] + "/" + pathParts[2], | ||
} | ||
|
||
productId, err := c.tokopediaService.GetProductId(ctx, productReq) | ||
if err != nil { | ||
res := utils.BuildResponseFailed(dto.MESSAGE_FAILED_GET_PRODUCT_ID, err.Error(), nil) | ||
ctx.JSON(http.StatusBadRequest, res) | ||
return | ||
} | ||
|
||
reviewsReq := dto.GetReviewsRequest{ | ||
ProductUrl: productReq.ProductUrl, | ||
ProductId: productId, | ||
} | ||
|
||
result, err := c.tokopediaService.GetReviews(ctx, reviewsReq) | ||
if err != nil { | ||
res := utils.BuildResponseFailed(dto.MESSAGE_FAILED_GET_REVIEWS, err.Error(), nil) | ||
ctx.JSON(http.StatusBadRequest, res) | ||
return | ||
} | ||
|
||
res := utils.BuildResponseSuccess(dto.MESSAGE_SUCCESS_GET_REVIEWS, result) | ||
ctx.JSON(http.StatusOK, res) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
package dto | ||
|
||
import "errors" | ||
|
||
const ( | ||
// Failed | ||
MESSAGE_FAILED_PARSE_URL = "failed parse url" | ||
MESSAGE_FAILED_SPLIT_URL = "failed split url" | ||
MESSAGE_FAILED_GET_PRODUCT_ID = "failed get product id" | ||
MESSAGE_FAILED_GET_REVIEWS = "failed get product reviews" | ||
|
||
// Success | ||
MESSAGE_SUCCESS_GET_REVIEWS = "success get reviews" | ||
) | ||
|
||
var ( | ||
ErrProductUrlMissing = errors.New("product url is required") | ||
ErrProductUrlWrongFormat = errors.New("invalid product url format") | ||
ErrCreateTokopediaRequest = errors.New("failed to create http request") | ||
ErrSendsTokopediaRequest = errors.New("failed to sends http request") | ||
ErrReadTokopediaResponseBody = errors.New("failed to read http response body") | ||
ErrParseJson = errors.New("failed to parse response json") | ||
ErrProductId = errors.New("failed to extract product id") | ||
) | ||
|
||
type ProductReviewResponseTokopedia struct { | ||
Data struct { | ||
ProductrevGetProductReviewList struct { | ||
List []struct { | ||
Message string `json:"message"` | ||
ProductRating int `json:"productRating"` | ||
} `json:"list"` | ||
} `json:"productrevGetProductReviewList"` | ||
} `json:"data"` | ||
} | ||
|
||
type GetProductIdRequest struct { | ||
ProductUrl string | ||
ProductKey string | ||
ShopDomain string | ||
} | ||
|
||
type GetReviewsRequest struct { | ||
ProductUrl string | ||
ProductId string | ||
} | ||
|
||
type ReviewResponse struct { | ||
Message string `json:"message"` | ||
Rating int `json:"rating"` | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
package routes | ||
|
||
import ( | ||
"ulascan-be/controller" | ||
"ulascan-be/service" | ||
|
||
"github.com/gin-gonic/gin" | ||
) | ||
|
||
func Tokopedia(route *gin.Engine, tokopediaController controller.TokopediaController, jwtService service.JWTService) { | ||
routes := route.Group("/api/tokopedia") | ||
{ | ||
routes.GET("/reviews", tokopediaController.GetReviews) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,144 @@ | ||
package service | ||
|
||
import ( | ||
"context" | ||
"encoding/json" | ||
"fmt" | ||
"io" | ||
"net/http" | ||
"strings" | ||
|
||
"ulascan-be/dto" | ||
) | ||
|
||
type ( | ||
TokopediaService interface { | ||
GetProductId(ctx context.Context, req dto.GetProductIdRequest) (string, error) | ||
GetReviews(ctx context.Context, req dto.GetReviewsRequest) ([]dto.ReviewResponse, error) | ||
} | ||
|
||
tokopediaService struct { | ||
} | ||
) | ||
|
||
func NewTokopediaService() TokopediaService { | ||
return &tokopediaService{} | ||
} | ||
|
||
func (s *tokopediaService) GetProductId(ctx context.Context, req dto.GetProductIdRequest) (string, error) { | ||
url := "https://gql.tokopedia.com/graphql/" | ||
method := "POST" | ||
|
||
payload := strings.NewReader(fmt.Sprintf(`{ | ||
"operationName": "PDPGetLayoutQuery", | ||
"variables": { | ||
"shopDomain": "%s", | ||
"productKey": "%s", | ||
"apiVersion": 1 | ||
}, | ||
"query": "query PDPGetLayoutQuery($shopDomain: String, $productKey: String, $apiVersion: Float) {\n pdpGetLayout(shopDomain: $shopDomain, productKey: $productKey, apiVersion: $apiVersion) {\n basicInfo {\n id: productID\n }\n }\n}\n" | ||
}`, req.ShopDomain, req.ProductKey)) | ||
|
||
client := &http.Client{} | ||
tokopediaReq, err := http.NewRequest(method, url, payload) | ||
if err != nil { | ||
fmt.Println(err) | ||
return "", dto.ErrCreateTokopediaRequest | ||
} | ||
|
||
tokopediaReq.Header.Add("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36") | ||
tokopediaReq.Header.Add("X-Source", "tokopedia-lite") | ||
tokopediaReq.Header.Add("X-Tkpd-Lite-Service", "zeus") | ||
tokopediaReq.Header.Add("Referer", req.ProductUrl) | ||
tokopediaReq.Header.Add("X-TKPD-AKAMAI", "pdpGetLayout") | ||
tokopediaReq.Header.Add("Content-Type", "application/json") | ||
|
||
res, err := client.Do(tokopediaReq) | ||
if err != nil { | ||
return "", dto.ErrSendsTokopediaRequest | ||
} | ||
defer res.Body.Close() | ||
|
||
body, err := io.ReadAll(res.Body) | ||
if err != nil { | ||
return "", dto.ErrReadTokopediaResponseBody | ||
} | ||
|
||
var response map[string]interface{} | ||
err = json.Unmarshal(body, &response) | ||
if err != nil { | ||
return "", dto.ErrParseJson | ||
} | ||
|
||
id, ok := response["data"].(map[string]interface{})["pdpGetLayout"].(map[string]interface{})["basicInfo"].(map[string]interface{})["id"].(string) | ||
if !ok { | ||
return "", dto.ErrProductId | ||
} | ||
|
||
return id, nil | ||
} | ||
|
||
func (s *tokopediaService) GetReviews(ctx context.Context, req dto.GetReviewsRequest) ([]dto.ReviewResponse, error) { | ||
url := "https://gql.tokopedia.com/graphql/" | ||
method := "POST" | ||
|
||
var allReviews []dto.ReviewResponse | ||
|
||
for page := 1; page <= 2; page++ { | ||
// Prepare the request payload | ||
payload := fmt.Sprintf(`{ | ||
"operationName": "productReviewList", | ||
"variables": { | ||
"productID": "%s", | ||
"page": %d, | ||
"limit": 50, | ||
"sortBy": "create_time desc" | ||
}, | ||
"query": "query productReviewList($productID: String!, $page: Int!, $limit: Int!, $sortBy: String) {\n productrevGetProductReviewList(productID: $productID, page: $page, limit: $limit, sortBy: $sortBy) {\n list {\n message\n productRating\n }\n }\n}\n" | ||
}`, req.ProductId, page) | ||
|
||
client := &http.Client{} | ||
|
||
tokopediaReq, err := http.NewRequest(method, url, strings.NewReader(payload)) | ||
if err != nil { | ||
return nil, dto.ErrCreateTokopediaRequest | ||
} | ||
|
||
tokopediaReq.Header.Add("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36") | ||
tokopediaReq.Header.Add("X-Source", "tokopedia-lite") | ||
tokopediaReq.Header.Add("X-Tkpd-Lite-Service", "zeus") | ||
tokopediaReq.Header.Add("Referer", req.ProductUrl) | ||
tokopediaReq.Header.Add("Content-Type", "application/json") | ||
|
||
res, err := client.Do(tokopediaReq) | ||
if err != nil { | ||
return nil, dto.ErrSendsTokopediaRequest | ||
} | ||
defer res.Body.Close() | ||
|
||
body, err := io.ReadAll(res.Body) | ||
if err != nil { | ||
return nil, dto.ErrReadTokopediaResponseBody | ||
} | ||
|
||
var response dto.ProductReviewResponseTokopedia | ||
err = json.Unmarshal(body, &response) | ||
if err != nil { | ||
return nil, dto.ErrParseJson | ||
} | ||
|
||
reviews := response.Data.ProductrevGetProductReviewList.List | ||
for _, review := range reviews { | ||
allReviews = append(allReviews, dto.ReviewResponse{ | ||
Message: review.Message, | ||
Rating: review.ProductRating, | ||
}) | ||
} | ||
|
||
if len(reviews) < 50 { | ||
break | ||
} | ||
} | ||
|
||
return allReviews, nil | ||
} |