diff --git a/README.md b/README.md index c3967c99..1e498e56 100644 --- a/README.md +++ b/README.md @@ -106,6 +106,8 @@ Those are the current existing endpoints. - GET `/v3/highscores/world/:world` - GET `/v3/highscores/world/:world/:category` - GET `/v3/highscores/world/:world/:category/:vocation` +- GET `/v3/houses/world/:world/house/:houseid` +- GET `/v3/houses/world/:world/town/:town` - GET `/v3/killstatistics/world/:world` - GET `/v3/news/archive` - GET `/v3/news/archive/:days` diff --git a/src/HousesMapping.go b/src/HousesMapping.go new file mode 100644 index 00000000..8cf741d8 --- /dev/null +++ b/src/HousesMapping.go @@ -0,0 +1,76 @@ +package main + +import ( + "encoding/json" + "log" + "net/http" + "time" + + "github.com/go-resty/resty/v2" +) + +var ( + TibiadataHousesMapping HousesMapping +) + +type AssetsHouse struct { + HouseID int `json:"house_id"` + Town string `json:"town"` + HouseType string `json:"type"` +} +type HousesMapping struct { + Houses []AssetsHouse `json:"houses"` +} + +// TibiaDataHousesMappingInitiator func - used to load data from local JSON file +func TibiaDataHousesMappingInitiator() { + + // Setting up resty client + client := resty.New() + + // Set client timeout and retry + client.SetTimeout(5 * time.Second) + client.SetRetryCount(2) + + // Set headers for all requests + client.SetHeaders(map[string]string{ + "Content-Type": "application/json", + "User-Agent": TibiadataUserAgent, + }) + + // Enabling Content length value for all request + client.SetContentLength(true) + + // Disable redirection of client (so we skip parsing maintenance page) + client.SetRedirectPolicy(resty.NoRedirectPolicy()) + + TibiadataAssetsURL := "https://raw.githubusercontent.com/TibiaData/tibiadata-api-assets/main/data/houses_mapping.json" + res, err := client.R().Get(TibiadataAssetsURL) + + switch res.StatusCode() { + case http.StatusOK: + // adding response into the data field + data := HousesMapping{} + err = json.Unmarshal([]byte(res.Body()), &data) + + if err != nil { + log.Println("[error] TibiaData API failed to parse content from houses_mapping.json") + } else { + // storing data so it's accessible from other places + TibiadataHousesMapping = data + } + + default: + log.Printf("[error] TibiaData API failed to load houses mapping. %s", err) + } +} + +// TibiaDataHousesMapResolver func - used to return both town and type +func TibiaDataHousesMapResolver(houseid int) (town string, housetype string) { + for _, value := range TibiadataHousesMapping.Houses { + if houseid == value.HouseID { + return value.Town, value.HouseType + } + } + return "", "" +} diff --git a/src/TibiaDataUtils.go b/src/TibiaDataUtils.go index dda9e3d7..663ee0b0 100644 --- a/src/TibiaDataUtils.go +++ b/src/TibiaDataUtils.go @@ -222,6 +222,11 @@ func getEnvAsInt(name string, defaultVal int) int { } */ +// TibiaDataConvertValuesWithK func - convert price strings that contain k, kk or more to 3x0 +func TibiaDataConvertValuesWithK(data string) int { + return TibiadataStringToIntegerV3(strings.ReplaceAll(data, "k", "") + strings.Repeat("000", strings.Count(data, "k"))) +} + // TibiaDataVocationValidator func - return valid vocation string and vocation id func TibiaDataVocationValidator(vocation string) (string, string) { // defining return vars diff --git a/src/TibiaHousesHouseV3.go b/src/TibiaHousesHouseV3.go new file mode 100644 index 00000000..d6e1d9d5 --- /dev/null +++ b/src/TibiaHousesHouseV3.go @@ -0,0 +1,193 @@ +package main + +import ( + "log" + "net/http" + "regexp" + "strings" + + "github.com/PuerkitoBio/goquery" + "github.com/gin-gonic/gin" +) + +// Child of Status +type HouseRental struct { + Owner string `json:"owner"` + OwnerSex string `json:"owner_sex"` + PaidUntil string `json:"paid_until"` + MovingDate string `json:"moving_date"` + TransferReceiver string `json:"transfer_receiver"` + TransferPrice int `json:"transfer_price"` + TransferAccept bool `json:"transfer_accept"` +} + +// Child of Status +type HouseAuction struct { + CurrentBid int `json:"current_bid"` + CurrentBidder string `json:"current_bidder"` + AuctionOngoing bool `json:"auction_ongoing"` + AuctionEnd string `json:"auction_end"` +} + +// Child of House +type HouseStatus struct { + IsAuctioned bool `json:"is_auctioned"` + IsRented bool `json:"is_rented"` + IsMoving bool `json:"is_moving"` + IsTransfering bool `json:"is_transfering"` + Auction HouseAuction `json:"auction"` + Rental HouseRental `json:"rental"` + Original string `json:"original"` +} + +// Child of JSONData +type House struct { + Houseid int `json:"houseid"` + World string `json:"world"` + Town string `json:"town,omitempty"` + Name string `json:"name"` + Type string `json:"type,omitempty"` + Beds int `json:"beds"` + Size int `json:"size"` + Rent int `json:"rent"` + Img string `json:"img"` + Status HouseStatus `json:"status"` +} + +// +// The base includes two levels: Houses and Information +type HouseResponse struct { + House House `json:"house"` + Information Information `json:"information"` +} + +// TibiaHousesHouseV3 func +func TibiaHousesHouseV3(c *gin.Context) { + + // getting params from URL + world := c.Param("world") + houseid := c.Param("houseid") + + // Creating empty vars + var HouseData House + + // Adding fix for First letter to be upper and rest lower + world = TibiadataStringWorldFormatToTitleV3(world) + + // Getting data with TibiadataHTMLDataCollectorV3 + TibiadataRequest.URL = "https://www.tibia.com/community/?subtopic=houses&page=view&world=" + TibiadataQueryEscapeStringV3(world) + "&houseid=" + TibiadataQueryEscapeStringV3(houseid) + BoxContentHTML, err := TibiadataHTMLDataCollectorV3(TibiadataRequest) + + // return error (e.g. for maintenance mode) + if err != nil { + TibiaDataAPIHandleOtherResponse(c, http.StatusBadGateway, "TibiaHousesHouseV3", gin.H{"error": err.Error()}) + return + } + + // Loading HTML data into ReaderHTML for goquery with NewReader + ReaderHTML, err := goquery.NewDocumentFromReader(strings.NewReader(BoxContentHTML)) + if err != nil { + log.Fatal(err) + } + + // Running query over each div + HouseHTML, err := ReaderHTML.Find(".BoxContent table tr").First().Html() + + if err != nil { + log.Fatal(err) + } + + // Regex to get data for house + regex1 := regexp.MustCompile(`(.*)<\/b>.*This (house|guildhall) can.*to ([0-9]+) beds..*([0-9]+) square.*([0-9]+)([k]+).gold<\/b>.*on ([A-Za-z]+)<\/b>.(.*)<\/td>`) + subma1 := regex1.FindAllStringSubmatch(HouseHTML, -1) + + if len(subma1) > 0 { + HouseData.Houseid = TibiadataStringToIntegerV3(houseid) + HouseData.World = subma1[0][8] + + HouseData.Town, HouseData.Type = TibiaDataHousesMapResolver(HouseData.Houseid) + + HouseData.Name = TibiaDataSanitizeEscapedString(subma1[0][2]) + HouseData.Img = subma1[0][1] + HouseData.Beds = TibiadataStringToIntegerV3(subma1[0][4]) + HouseData.Size = TibiadataStringToIntegerV3(subma1[0][5]) + HouseData.Rent = TibiaDataConvertValuesWithK(subma1[0][6] + subma1[0][7]) + + HouseData.Status.Original = TibiaDataSanitizeEscapedString(RemoveHtmlTag(subma1[0][9])) + + switch { + case strings.Contains(HouseData.Status.Original, "has been rented by"): + // rented + + switch { + case strings.Contains(HouseData.Status.Original, " pass the "+HouseData.Type+" to "): + HouseData.Status.IsTransfering = true + // matching for this: and pass the to for gold + regex2 := regexp.MustCompile(`and (wants to|will) pass the (house|guildhall) to (.*) for ([0-9]+) gold`) + subma2 := regex2.FindAllStringSubmatch(HouseData.Status.Original, -1) + // storing values from regex + if subma2[0][1] == "will" { + HouseData.Status.Rental.TransferAccept = true + } + HouseData.Status.Rental.TransferReceiver = subma2[0][3] + HouseData.Status.Rental.TransferPrice = TibiadataStringToIntegerV3(subma2[0][4]) + fallthrough + + case strings.Contains(HouseData.Status.Original, " will move out on "): + HouseData.Status.IsMoving = true + // matching for this: will move out on ( + regex2 := regexp.MustCompile(`(He|She) will move out on (.*?) \(`) + subma2 := regex2.FindAllStringSubmatch(HouseData.Status.Original, -1) + // storing values from regex + HouseData.Status.Rental.MovingDate = TibiadataDatetimeV3(subma2[0][2]) + fallthrough + + default: + HouseData.Status.IsRented = true + // matching for this: The has been rented by . has paid the rent until . + regex2 := regexp.MustCompile(`The (house|guildhall) has been rented by (.*). (He|She) has paid.*until (.*?)\.`) + subma2 := regex2.FindAllStringSubmatch(HouseData.Status.Original, -1) + // storing values from regex + HouseData.Status.Rental.Owner = subma2[0][2] + HouseData.Status.Rental.PaidUntil = TibiadataDatetimeV3(subma2[0][4]) + switch subma2[0][3] { + case "She": + HouseData.Status.Rental.OwnerSex = "female" + case "He": + HouseData.Status.Rental.OwnerSex = "male" + } + } + + case strings.Contains(HouseData.Status.Original, "is currently being auctioned"): + // auctioned + HouseData.Status.IsAuctioned = true + + // check if bid is going on + if !strings.Contains(HouseData.Status.Original, "No bid has been submitted so far.") { + regex2 := regexp.MustCompile(`The (house|guildhall) is currently.*The auction (will end|has ended) at (.*)\. The.*is ([0-9]+) gold.*submitted by (.*)\.`) + subma2 := regex2.FindAllStringSubmatch(HouseData.Status.Original, -1) + // storing values from regex + HouseData.Status.Auction.AuctionEnd = TibiadataDatetimeV3(subma2[0][3]) + HouseData.Status.Auction.CurrentBid = TibiadataStringToIntegerV3(subma2[0][4]) + HouseData.Status.Auction.CurrentBidder = subma2[0][5] + if subma2[0][2] == "will end" { + HouseData.Status.Auction.AuctionOngoing = true + } + } + } + + } + + // + // Build the data-blob + jsonData := HouseResponse{ + HouseData, + Information{ + APIVersion: TibiadataAPIversion, + Timestamp: TibiadataDatetimeV3(""), + }, + } + + // return jsonData + TibiaDataAPIHandleSuccessResponse(c, "TibiaHousesHouseV3", jsonData) +} diff --git a/src/TibiaHousesOverviewV3.go b/src/TibiaHousesOverviewV3.go new file mode 100644 index 00000000..5553aa27 --- /dev/null +++ b/src/TibiaHousesOverviewV3.go @@ -0,0 +1,149 @@ +package main + +import ( + "log" + "net/http" + "regexp" + "strings" + + "github.com/PuerkitoBio/goquery" + "github.com/gin-gonic/gin" +) + +// Child of House +type HousesAction struct { + AuctionBid int `json:"current_bid"` + AuctionLeft string `json:"time_left"` +} + +// Child of HousesHouses +type HousesHouse struct { + Name string `json:"name"` + HouseID int `json:"house_id"` + Size int `json:"size"` + Rent int `json:"rent"` + IsRented bool `json:"rented"` + IsAuctioned bool `json:"auctioned"` + Auction HousesAction `json:"auction"` +} + +// Child of JSONData +type HousesHouses struct { + World string `json:"world"` + Town string `json:"town"` + HouseList []HousesHouse `json:"house_list"` + GuildhallList []HousesHouse `json:"guildhall_list"` +} + +// The base includes two levels: HousesHouses and Information +type HousesOverviewResponse struct { + Houses HousesHouses `json:"houses"` + Information Information `json:"information"` +} + +// TibiaHousesOverviewV3 func +func TibiaHousesOverviewV3(c *gin.Context) { + // getting params from URL + world := c.Param("world") + town := c.Param("town") + + // Adding fix for First letter to be upper and rest lower + world = TibiadataStringWorldFormatToTitleV3(world) + town = TibiadataStringWorldFormatToTitleV3(town) + + var ( + // Creating empty vars + HouseData, GuildhallData []HousesHouse + ) + + // list of different fansite types + HouseTypes := []string{"houses", "guildhalls"} + // running over the FansiteTypes array + for _, HouseType := range HouseTypes { + + // Getting data with TibiadataHTMLDataCollectorV3 + TibiadataRequest.URL = "https://www.tibia.com/community/?subtopic=houses&world=" + TibiadataQueryEscapeStringV3(world) + "&town=" + TibiadataQueryEscapeStringV3(town) + "&type=" + TibiadataQueryEscapeStringV3(HouseType) + BoxContentHTML, err := TibiadataHTMLDataCollectorV3(TibiadataRequest) + + // return error (e.g. for maintenance mode) + if err != nil { + TibiaDataAPIHandleOtherResponse(c, http.StatusBadGateway, "TibiaHousesOverviewV3", gin.H{"error": err.Error()}) + return + } + + // Loading HTML data into ReaderHTML for goquery with NewReader + ReaderHTML, err := goquery.NewDocumentFromReader(strings.NewReader(BoxContentHTML)) + if err != nil { + log.Fatal(err) + } + + ReaderHTML.Find(".TableContentContainer .TableContent tr").Each(func(index int, s *goquery.Selection) { + house := HousesHouse{} + + // Storing HTML into HousesDivHTML + HousesDivHTML, err := s.Html() + if err != nil { + log.Fatal(err) + } + + // Removing linebreaks from HTML + HousesDivHTML = TibiadataHTMLRemoveLinebreaksV3(HousesDivHTML) + HousesDivHTML = TibiaDataSanitizeNbspSpaceString(HousesDivHTML) + + // Regex to get data for record values + regex1 := regexp.MustCompile(`(.*)<\/nobr><\/td>([0-9]+).sqm<\/nobr><\/td>([0-9]+)(k+).gold<\/nobr><\/td>(.*)<\/nobr><\/td>.*houseid" value="([0-9]+)"\/> 0 { + // House details + house.Name = TibiaDataSanitizeEscapedString(subma1[0][1]) + house.HouseID = TibiadataStringToIntegerV3(subma1[0][6]) + house.Size = TibiadataStringToIntegerV3(subma1[0][2]) + house.Rent = TibiaDataConvertValuesWithK(subma1[0][3] + subma1[0][4]) + + // HousesAction details + s := subma1[0][5] + switch { + case strings.Contains(s, "rented"): + house.IsRented = true + case strings.Contains(s, "auctioned (no bid yet)"): + house.IsAuctioned = true + case strings.Contains(s, "auctioned"): + house.IsAuctioned = true + regex1b := regexp.MustCompile(`auctioned.\(([0-9]+).gold;.(.*).left\)`) + subma1b := regex1b.FindAllStringSubmatch(s, -1) + house.Auction.AuctionBid = TibiadataStringToIntegerV3(subma1b[0][1]) + house.Auction.AuctionLeft = subma1b[0][2] + } + + // append house to list houses/guildhalls + switch HouseType { + case "houses": + HouseData = append(HouseData, house) + case "guildhalls": + GuildhallData = append(GuildhallData, house) + } + + } + + }) + + } + + // Build the data-blob + jsonData := HousesOverviewResponse{ + HousesHouses{ + World: world, + Town: town, + HouseList: HouseData, + GuildhallList: GuildhallData, + }, + Information{ + APIVersion: TibiadataAPIversion, + Timestamp: TibiadataDatetimeV3(""), + }, + } + + // return jsonData + TibiaDataAPIHandleSuccessResponse(c, "TibiaHousesOverviewV3", jsonData) +} diff --git a/src/main.go b/src/main.go index bbd9335e..24374c1b 100644 --- a/src/main.go +++ b/src/main.go @@ -45,6 +45,7 @@ func main() { log.Printf("[debug] TIbiaData API User-Agent: %s", TibiadataUserAgent) } + // starting webserver.go stuff runWebServer() } @@ -70,4 +71,8 @@ func TibiaDataInitializer() { } log.Printf("[info] TibiaData API proxy: %s", TibiadataProxyDomain) + + // initializing houses mappings + TibiaDataHousesMappingInitiator() + } diff --git a/src/webserver.go b/src/webserver.go index e47fe710..6228c99d 100644 --- a/src/webserver.go +++ b/src/webserver.go @@ -101,6 +101,10 @@ func runWebServer() { }) v3.GET("/highscores/world/:world/:category/:vocation", TibiaHighscoresV3) + // Tibia houses + v3.GET("/houses/world/:world/house/:houseid", TibiaHousesHouseV3) + v3.GET("/houses/world/:world/town/:town", TibiaHousesOverviewV3) + // Tibia killstatistics v3.GET("/killstatistics/world/:world", TibiaKillstatisticsV3)