Skip to content

Commit

Permalink
Merge pull request #6 from Ulas-Scan/1-tokopedia-get-reviews
Browse files Browse the repository at this point in the history
feat: get product id, get reviews
  • Loading branch information
javakanaya authored Jun 6, 2024
2 parents b87ee94 + 119f26d commit c98ecaa
Show file tree
Hide file tree
Showing 8 changed files with 305 additions and 5 deletions.
80 changes: 80 additions & 0 deletions controller/tokopedia.go
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)
}
51 changes: 51 additions & 0 deletions dto/reviews.go
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"`
}
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ require (
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/rogpeppe/go-internal v1.12.0 // indirect
golang.org/x/sync v0.7.0 // indirect
)
Expand All @@ -36,6 +37,7 @@ require (
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/machinebox/graphql v0.2.2
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
Expand Down
4 changes: 4 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/machinebox/graphql v0.2.2 h1:dWKpJligYKhYKO5A2gvNhkJdQMNZeChZYyBbrZkBZfo=
github.com/machinebox/graphql v0.2.2/go.mod h1:F+kbVMHuwrQ5tYgU9JXlnskM8nOaFxCAEolaQybkjWA=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
Expand All @@ -68,6 +70,8 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
Expand Down
13 changes: 8 additions & 5 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,15 @@ func main() {
historyRepository repository.HistoryRepository = repository.NewHistoryRepository(db)

// SERVICE
jwtService service.JWTService = service.NewJWTService()
userService service.UserService = service.NewUserService(userRepository, jwtService)
historyService service.HistoryService = service.NewHistoryService(historyRepository)
jwtService service.JWTService = service.NewJWTService()
userService service.UserService = service.NewUserService(userRepository, jwtService)
historyService service.HistoryService = service.NewHistoryService(historyRepository)
tokopediaService service.TokopediaService = service.NewTokopediaService()

// CONTROLLER
userController controller.UserController = controller.NewUserController(userService)
historyController controller.HistoryController = controller.NewHistoryController(historyService)
userController controller.UserController = controller.NewUserController(userService)
historyController controller.HistoryController = controller.NewHistoryController(historyService)
tokopediaController controller.TokopediaController = controller.NewTokopediaController(tokopediaService)
)

defer config.CloseDatabaseConnection(db)
Expand Down Expand Up @@ -63,6 +65,7 @@ func main() {

// ROUTES
routes.User(server, userController, jwtService)
routes.Tokopedia(server, tokopediaController, jwtService)
routes.History(server, historyController, jwtService)

// RUNING THE SERVER
Expand Down
15 changes: 15 additions & 0 deletions routes/tokopedia.go
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)
}
}
1 change: 1 addition & 0 deletions routes/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,6 @@ func User(route *gin.Engine, userController controller.UserController, jwtServic
routes.DELETE("", middleware.Authenticate(jwtService), userController.Delete)
routes.PATCH("", middleware.Authenticate(jwtService), userController.Update)
routes.GET("/me", middleware.Authenticate(jwtService), userController.Me)

}
}
144 changes: 144 additions & 0 deletions service/tokopedia.go
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
}

0 comments on commit c98ecaa

Please sign in to comment.