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)