mirror of
https://github.com/idanoo/go-flat-finder
synced 2025-07-01 05:32:17 +00:00
Initial Commit
This commit is contained in:
commit
ee32ba1537
13 changed files with 583 additions and 0 deletions
12
.env.example
Normal file
12
.env.example
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
DISCORD_WEBHOOK=""
|
||||||
|
GOOGLE_API_KEY=""
|
||||||
|
GOOGLE_LOCATION_1="42 Wallaby Way, Sydney"
|
||||||
|
GOOGLE_LOCATION_2="43 Wallaby Way, Sydney"
|
||||||
|
TRADEME_API_KEY=""
|
||||||
|
TRADEME_API_SECRET=""
|
||||||
|
SUBURBS="47,52"
|
||||||
|
BEDROOMS_MIN="2"
|
||||||
|
BEDROOMS_MAX="4"
|
||||||
|
PRICE_MAX="700"
|
||||||
|
PROPERTY_TYPE="House,Townhouse,Apartment"
|
||||||
|
# https://developer.trademe.co.nz/api-reference/search-methods/rental-search
|
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
/flatfinder
|
||||||
|
/old
|
||||||
|
.env
|
||||||
|
flatfinder.json
|
9
.woodpecker/.build.yml
Normal file
9
.woodpecker/.build.yml
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
pipeline:
|
||||||
|
build:
|
||||||
|
image: golang:1.19.1
|
||||||
|
commands:
|
||||||
|
- go mod tidy
|
||||||
|
|
||||||
|
depends_on:
|
||||||
|
- lint
|
||||||
|
- test
|
5
.woodpecker/.lint.yml
Normal file
5
.woodpecker/.lint.yml
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
pipeline:
|
||||||
|
lint:
|
||||||
|
image: golang:1.19.1
|
||||||
|
commands:
|
||||||
|
- go mod tidy
|
8
.woodpecker/.test.yml
Normal file
8
.woodpecker/.test.yml
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
pipeline:
|
||||||
|
tests:
|
||||||
|
image: golang:1.19.1
|
||||||
|
commands:
|
||||||
|
- go mod tidy
|
||||||
|
|
||||||
|
depends_on:
|
||||||
|
- lint
|
35
README.md
Normal file
35
README.md
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
# FlatFinder Bot
|
||||||
|
|
||||||
|
* Uses the Trade Me API to grab new rental properties that have been recently listed
|
||||||
|
* Checks if fibre and VDSL are available by querying Chorus
|
||||||
|
* Includes travel times to various locations
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
* Linux environment
|
||||||
|
* Trade Me API Key [(register an application)](https://www.trademe.co.nz/MyTradeMe/Api/RegisterNewApplication.aspx)
|
||||||
|
|
||||||
|
## Optionals
|
||||||
|
* Google Distance Matrix API Key [(get a key)](https://developers.google.com/maps/documentation/distance-matrix/start#get-a-key)
|
||||||
|
* Discord API key
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
* Download latest build
|
||||||
|
* Run with below exe with environment variables set
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
Copy `.env.example`to `.env` and set variables. Leave blank to disable parts.
|
||||||
|
|
||||||
|
```
|
||||||
|
SINCE="2 hours ago"
|
||||||
|
DISCORD_WEBHOOK="abcd"
|
||||||
|
GOOGLE_API_KEY="abcd"
|
||||||
|
GOOGLE_LOCATION_1="42 Wallaby Way, Sydney"
|
||||||
|
GOOGLE_LOCATION_2="43 Wallaby Way, Sydney"
|
||||||
|
DISTRICTS="47,52"
|
||||||
|
BEDROOMS_MIN="2"
|
||||||
|
BEDROOMS_MAX="4"
|
||||||
|
PRICE_MAX="700"
|
||||||
|
PROPERTY_TYPE="House,Townhouse,Apartment"
|
||||||
|
```
|
||||||
|
|
||||||
|
Reference: [http://developer.trademe.co.nz/api-reference/search-methods/rental-search/](http://developer.trademe.co.nz/api-reference/search-methods/rental-search/)
|
72
cmd/flatfinder/main.go
Executable file
72
cmd/flatfinder/main.go
Executable file
|
@ -0,0 +1,72 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"flatfinder/internal/flatfinder"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/joho/godotenv"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
// Load .env
|
||||||
|
err := godotenv.Load()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal("Cannot load .env file in current directory")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
// Load env vars and validate
|
||||||
|
flatfinder.Conf = flatfinder.LocalConfig{}
|
||||||
|
|
||||||
|
// Load webhook
|
||||||
|
flatfinder.Conf.DiscordWebhook = os.Getenv("DISCORD_WEBHOOK")
|
||||||
|
if flatfinder.Conf.DiscordWebhook == "" {
|
||||||
|
log.Fatal("DISCORD_WEBHOOK not set")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load Google stuff
|
||||||
|
flatfinder.Conf.GoogleApiToken = os.Getenv("GOOGLE_API_KEY")
|
||||||
|
if flatfinder.Conf.GoogleApiToken == "" {
|
||||||
|
log.Print("GOOGLE_API_KEY not set. Not using map logicc")
|
||||||
|
}
|
||||||
|
flatfinder.Conf.GoogleLocation1 = os.Getenv("GOOGLE_LOCATION_1")
|
||||||
|
flatfinder.Conf.GoogleLocation2 = os.Getenv("GOOGLE_LOCATION_2")
|
||||||
|
|
||||||
|
// Load trademe config
|
||||||
|
flatfinder.Conf.TradeMeKey = os.Getenv("TRADEME_API_KEY")
|
||||||
|
flatfinder.Conf.TradeMeSecret = os.Getenv("TRADEME_API_SECRET")
|
||||||
|
if flatfinder.Conf.TradeMeKey == "" || flatfinder.Conf.TradeMeSecret == "" {
|
||||||
|
log.Fatal("TRADEME_API_KEY or TRADEME_API_SECRET not set")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load filterse
|
||||||
|
flatfinder.Conf.Suburbs = os.Getenv("SUBURBS")
|
||||||
|
if flatfinder.Conf.Suburbs == "" {
|
||||||
|
log.Fatal("SUBURBS not set")
|
||||||
|
}
|
||||||
|
|
||||||
|
flatfinder.Conf.BedroomsMin = os.Getenv("BEDROOMS_MIN")
|
||||||
|
if flatfinder.Conf.BedroomsMin == "" {
|
||||||
|
log.Fatal("BEDROOMS_MIN not set")
|
||||||
|
}
|
||||||
|
|
||||||
|
flatfinder.Conf.BedroomsMax = os.Getenv("BEDROOMS_MAX")
|
||||||
|
if flatfinder.Conf.BedroomsMax == "" {
|
||||||
|
log.Fatal("BEDROOMS_MAX not set")
|
||||||
|
}
|
||||||
|
|
||||||
|
flatfinder.Conf.PriceMax = os.Getenv("PRICE_MAX")
|
||||||
|
if flatfinder.Conf.PriceMax == "" {
|
||||||
|
log.Fatal("PRICE_MAX not set")
|
||||||
|
}
|
||||||
|
|
||||||
|
flatfinder.Conf.PropertyTypes = os.Getenv("PROPERTY_TYPE")
|
||||||
|
if flatfinder.Conf.PropertyTypes == "" {
|
||||||
|
log.Fatal("PROPERTY_TYPE not set")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start the stuff
|
||||||
|
flatfinder.Launch()
|
||||||
|
}
|
18
go.mod
Normal file
18
go.mod
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
module flatfinder
|
||||||
|
|
||||||
|
go 1.19
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/bwmarrin/discordgo v0.26.1
|
||||||
|
github.com/disgoorg/disgo v0.13.19
|
||||||
|
github.com/disgoorg/snowflake/v2 v2.0.0
|
||||||
|
github.com/joho/godotenv v1.4.0
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/disgoorg/log v1.2.0 // indirect
|
||||||
|
github.com/gorilla/websocket v1.5.0 // indirect
|
||||||
|
github.com/sasha-s/go-csync v0.0.0-20210812194225-61421b77c44b // indirect
|
||||||
|
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b // indirect
|
||||||
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68 // indirect
|
||||||
|
)
|
27
go.sum
Normal file
27
go.sum
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
github.com/bwmarrin/discordgo v0.26.1 h1:AIrM+g3cl+iYBr4yBxCBp9tD9jR3K7upEjl0d89FRkE=
|
||||||
|
github.com/bwmarrin/discordgo v0.26.1/go.mod h1:NJZpH+1AfhIcyQsPeuBKsUtYrRnjkyu0kIVMCHkZtRY=
|
||||||
|
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
|
||||||
|
github.com/disgoorg/disgo v0.13.19 h1:evbkWRQ9fU3dIrRJnl+jFTt55cKAeeEwnB/Q3dqgz20=
|
||||||
|
github.com/disgoorg/disgo v0.13.19/go.mod h1:Cyip4bCYHD3rHgDhBPT9cLo81e9AMbDe8ocM50UNRM4=
|
||||||
|
github.com/disgoorg/log v1.2.0 h1:sqlXnu/ZKAlIlHV9IO+dbMto7/hCQ474vlIdMWk8QKo=
|
||||||
|
github.com/disgoorg/log v1.2.0/go.mod h1:3x1KDG6DI1CE2pDwi3qlwT3wlXpeHW/5rVay+1qDqOo=
|
||||||
|
github.com/disgoorg/snowflake/v2 v2.0.0 h1:+xvyyDddXmXLHmiG8SZiQ3sdZdZPbUR22fSHoqwkrOA=
|
||||||
|
github.com/disgoorg/snowflake/v2 v2.0.0/go.mod h1:SPU9c2CNn5DSyb86QcKtdZgix9osEtKrHLW4rMhfLCs=
|
||||||
|
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||||
|
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
|
||||||
|
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||||
|
github.com/joho/godotenv v1.4.0 h1:3l4+N6zfMWnkbPEXKng2o2/MR5mSwTrBih4ZEkkz1lg=
|
||||||
|
github.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/sasha-s/go-csync v0.0.0-20210812194225-61421b77c44b h1:qYTY2tN72LhgDj2rtWG+LI6TXFl2ygFQQ4YezfVaGQE=
|
||||||
|
github.com/sasha-s/go-csync v0.0.0-20210812194225-61421b77c44b/go.mod h1:/pA7k3zsXKdjjAiUhB5CjuKib9KJGCaLvZwtxGC8U0s=
|
||||||
|
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
|
||||||
|
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b h1:7mWr3k41Qtv8XlltBkDkl8LoP3mpSgBW8BUoxtEdbXg=
|
||||||
|
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
|
||||||
|
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68 h1:nxC68pudNYkKU6jWhgrqdreuFiOQWj1Fs7T3VrH4Pjw=
|
||||||
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
|
54
internal/flatfinder/discord.go
Normal file
54
internal/flatfinder/discord.go
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
package flatfinder
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/disgoorg/disgo/discord"
|
||||||
|
"github.com/disgoorg/disgo/webhook"
|
||||||
|
"github.com/disgoorg/snowflake/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Load discord client
|
||||||
|
func (c *LocalConfig) initDiscord() {
|
||||||
|
// Webhook URL splitting
|
||||||
|
webhookString := strings.ReplaceAll(c.DiscordWebhook, "https://discord.com/api/webhooks/", "")
|
||||||
|
webhookParts := strings.Split(webhookString, "/")
|
||||||
|
if len(webhookParts) != 2 {
|
||||||
|
log.Fatal("Invalid DISCORD_WEBHOOK")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert snowflakeID to uint64
|
||||||
|
i, err := strconv.ParseInt(webhookParts[0], 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start client!
|
||||||
|
client := webhook.New(snowflake.ID(i), webhookParts[1])
|
||||||
|
c.DiscordClient = client
|
||||||
|
|
||||||
|
log.Print("Discord client loaded succesfully")
|
||||||
|
}
|
||||||
|
|
||||||
|
// sendEmbeddedMessage - Build an embedded message from listing data
|
||||||
|
func (c *LocalConfig) sendEmbeddedMessage(listing TradeMeListing) {
|
||||||
|
log.Printf("New listing: %s", listing.Title)
|
||||||
|
|
||||||
|
embed := discord.NewEmbedBuilder().
|
||||||
|
SetTitle(listing.Title).
|
||||||
|
SetURL(fmt.Sprintf("https://trademe.co.nz/%d", listing.ListingID)).
|
||||||
|
SetColor(1127128).
|
||||||
|
SetImage(listing.PictureHref).
|
||||||
|
AddField("Location", listing.Address, true).
|
||||||
|
AddField("Bedrooms", fmt.Sprintf("%d", listing.Bedrooms), true)
|
||||||
|
|
||||||
|
embeds := []discord.Embed{}
|
||||||
|
embeds = append(embeds, embed.Build())
|
||||||
|
_, err := c.DiscordClient.CreateEmbeds(embeds)
|
||||||
|
if err != nil {
|
||||||
|
log.Print(err)
|
||||||
|
}
|
||||||
|
}
|
70
internal/flatfinder/main.go
Normal file
70
internal/flatfinder/main.go
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
package flatfinder
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/disgoorg/disgo/webhook"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Our local struct we will store data during runtime
|
||||||
|
type LocalConfig struct {
|
||||||
|
DiscordWebhook string `json:"-"`
|
||||||
|
DiscordClient webhook.Client `json:"-"`
|
||||||
|
|
||||||
|
GoogleApiToken string `json:"-"`
|
||||||
|
GoogleLocation1 string `json:"-"`
|
||||||
|
GoogleLocation2 string `json:"-"`
|
||||||
|
|
||||||
|
TradeMeKey string `json:"-"`
|
||||||
|
TradeMeSecret string `json:"-"`
|
||||||
|
|
||||||
|
Suburbs string `json:"-"`
|
||||||
|
BedroomsMin string `json:"-"`
|
||||||
|
BedroomsMax string `json:"-"`
|
||||||
|
PriceMax string `json:"-"`
|
||||||
|
PropertyTypes string `json:"-"`
|
||||||
|
|
||||||
|
PostedProperties map[int64]bool `json:"properties"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var Conf LocalConfig
|
||||||
|
|
||||||
|
// Launch!
|
||||||
|
func Launch() {
|
||||||
|
// Load discord
|
||||||
|
Conf.initDiscord()
|
||||||
|
|
||||||
|
// Load previously posted properties
|
||||||
|
Conf.loadConfig()
|
||||||
|
|
||||||
|
// Intial run
|
||||||
|
Conf.pollUpdates()
|
||||||
|
|
||||||
|
// Run every minute!
|
||||||
|
ticker := time.NewTicker(1 * time.Minute)
|
||||||
|
quit := make(chan struct{})
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ticker.C:
|
||||||
|
Conf.pollUpdates()
|
||||||
|
case <-quit:
|
||||||
|
ticker.Stop()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// pollUpdates - check for new listings!
|
||||||
|
func (c *LocalConfig) pollUpdates() {
|
||||||
|
log.Printf("Polling for updates")
|
||||||
|
|
||||||
|
err := Conf.searchTrademe()
|
||||||
|
if err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update config
|
||||||
|
c.storeConfig()
|
||||||
|
}
|
186
internal/flatfinder/trademe.go
Normal file
186
internal/flatfinder/trademe.go
Normal file
|
@ -0,0 +1,186 @@
|
||||||
|
package flatfinder
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
var TradeMeBaseURL = "https://api.trademe.co.nz/v1/Search/Property/Rental.json"
|
||||||
|
|
||||||
|
// https://developer.trademe.co.nz/api-reference/search-methods/rental-search
|
||||||
|
type TrademeResultSet struct {
|
||||||
|
TotalCount int `json:"TotalCount"`
|
||||||
|
Page int `json:"Page"`
|
||||||
|
PageSize int `json:"PageSize"`
|
||||||
|
List []TradeMeListing `json:"List"`
|
||||||
|
FoundCategories []interface{} `json:"FoundCategories"`
|
||||||
|
SearchQueryID string `json:"SearchQueryId"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type TradeMeListing struct {
|
||||||
|
ListingID int64 `json:"ListingId"`
|
||||||
|
Title string `json:"Title"`
|
||||||
|
Category string `json:"Category"`
|
||||||
|
StartPrice int `json:"StartPrice"`
|
||||||
|
StartDate string `json:"StartDate"`
|
||||||
|
EndDate string `json:"EndDate"`
|
||||||
|
ListingLength interface{} `json:"ListingLength"`
|
||||||
|
IsFeatured bool `json:"IsFeatured,omitempty"`
|
||||||
|
HasGallery bool `json:"HasGallery"`
|
||||||
|
IsBold bool `json:"IsBold,omitempty"`
|
||||||
|
IsHighlighted bool `json:"IsHighlighted,omitempty"`
|
||||||
|
AsAt string `json:"AsAt"`
|
||||||
|
CategoryPath string `json:"CategoryPath"`
|
||||||
|
PictureHref string `json:"PictureHref"`
|
||||||
|
RegionID int `json:"RegionId"`
|
||||||
|
Region string `json:"Region"`
|
||||||
|
SuburbID int `json:"SuburbId"`
|
||||||
|
Suburb string `json:"Suburb"`
|
||||||
|
NoteDate string `json:"NoteDate"`
|
||||||
|
ReserveState int `json:"ReserveState"`
|
||||||
|
IsClassified bool `json:"IsClassified"`
|
||||||
|
OpenHomes []interface{} `json:"OpenHomes"`
|
||||||
|
GeographicLocation struct {
|
||||||
|
Latitude float64 `json:"Latitude"`
|
||||||
|
Longitude float64 `json:"Longitude"`
|
||||||
|
Northing int `json:"Northing"`
|
||||||
|
Easting int `json:"Easting"`
|
||||||
|
Accuracy int `json:"Accuracy"`
|
||||||
|
} `json:"GeographicLocation"`
|
||||||
|
PriceDisplay string `json:"PriceDisplay"`
|
||||||
|
PhotoUrls []string `json:"PhotoUrls"`
|
||||||
|
AdditionalData struct {
|
||||||
|
BulletPoints []interface{} `json:"BulletPoints"`
|
||||||
|
Tags []interface{} `json:"Tags"`
|
||||||
|
} `json:"AdditionalData"`
|
||||||
|
ListingExtras []string `json:"ListingExtras"`
|
||||||
|
MemberID int `json:"MemberId"`
|
||||||
|
Address string `json:"Address"`
|
||||||
|
District string `json:"District"`
|
||||||
|
AvailableFrom string `json:"AvailableFrom"`
|
||||||
|
Bathrooms int `json:"Bathrooms"`
|
||||||
|
Bedrooms int `json:"Bedrooms"`
|
||||||
|
ListingGroup string `json:"ListingGroup"`
|
||||||
|
Parking string `json:"Parking"`
|
||||||
|
PetsOkay int `json:"PetsOkay"`
|
||||||
|
PropertyType string `json:"PropertyType"`
|
||||||
|
RentPerWeek int `json:"RentPerWeek"`
|
||||||
|
SmokersOkay int `json:"SmokersOkay"`
|
||||||
|
Whiteware string `json:"Whiteware"`
|
||||||
|
AdjacentSuburbNames []string `json:"AdjacentSuburbNames"`
|
||||||
|
AdjacentSuburbIds []int `json:"AdjacentSuburbIds"`
|
||||||
|
DistrictID int `json:"DistrictId"`
|
||||||
|
Agency struct {
|
||||||
|
ID int `json:"Id"`
|
||||||
|
Name string `json:"Name"`
|
||||||
|
Website string `json:"Website"`
|
||||||
|
Logo string `json:"Logo"`
|
||||||
|
Branding struct {
|
||||||
|
BackgroundColor string `json:"BackgroundColor"`
|
||||||
|
TextColor string `json:"TextColor"`
|
||||||
|
StrokeColor string `json:"StrokeColor"`
|
||||||
|
OfficeLocation string `json:"OfficeLocation"`
|
||||||
|
LargeBannerURL string `json:"LargeBannerURL"`
|
||||||
|
} `json:"Branding"`
|
||||||
|
Logo2 string `json:"Logo2"`
|
||||||
|
Agents []struct {
|
||||||
|
FullName string `json:"FullName"`
|
||||||
|
} `json:"Agents"`
|
||||||
|
IsRealEstateAgency bool `json:"IsRealEstateAgency"`
|
||||||
|
} `json:"Agency,omitempty"`
|
||||||
|
TotalParking int `json:"TotalParking"`
|
||||||
|
IsSuperFeatured bool `json:"IsSuperFeatured"`
|
||||||
|
AgencyReference string `json:"AgencyReference"`
|
||||||
|
BestContactTime string `json:"BestContactTime"`
|
||||||
|
IdealTenant string `json:"IdealTenant"`
|
||||||
|
MaxTenants int `json:"MaxTenants"`
|
||||||
|
PropertyID string `json:"PropertyId"`
|
||||||
|
Amenities string `json:"Amenities"`
|
||||||
|
Lounges int `json:"Lounges"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *LocalConfig) searchTrademe() error {
|
||||||
|
// Only show last 2 hours of posts
|
||||||
|
dateFrom := time.Now().Add(-time.Hour * 6)
|
||||||
|
|
||||||
|
// Set filters
|
||||||
|
queryParams := url.Values{}
|
||||||
|
queryParams.Add("photo_size", "FullSize") // 670x502
|
||||||
|
queryParams.Add("sort_order", "Default") // Standard order
|
||||||
|
queryParams.Add("return_metadata", "false") // Include search data
|
||||||
|
queryParams.Add("rows", "500") // Total results
|
||||||
|
|
||||||
|
queryParams.Add("date_from", dateFrom.Format("2006-01-02T15:00"))
|
||||||
|
queryParams.Add("suburb", c.Suburbs)
|
||||||
|
queryParams.Add("property_type", c.PropertyTypes)
|
||||||
|
queryParams.Add("price_max", c.PriceMax)
|
||||||
|
queryParams.Add("bedrooms_min", c.BedroomsMin)
|
||||||
|
queryParams.Add("bedrooms_max", c.BedroomsMax)
|
||||||
|
|
||||||
|
// Build HTTP request
|
||||||
|
client := http.Client{}
|
||||||
|
req, err := http.NewRequest("GET", TradeMeBaseURL, nil)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Append our filters
|
||||||
|
req.URL.RawQuery = queryParams.Encode()
|
||||||
|
|
||||||
|
// Auth
|
||||||
|
req.Header.Set("Authorization", "OAuth oauth_consumer_key=\""+c.TradeMeKey+"\", oauth_signature_method=\"PLAINTEXT\", oauth_signature=\""+c.TradeMeSecret+"&\"")
|
||||||
|
|
||||||
|
req.Header.Set("Content-TypeContent-Type", "application/json")
|
||||||
|
req.Header.Set("User-Agent", "https://tinker.nz/idanoo/flat-finder")
|
||||||
|
|
||||||
|
// Do the request
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode == http.StatusOK {
|
||||||
|
bodyBytes, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.handleTrademeResponse(bodyBytes)
|
||||||
|
} else {
|
||||||
|
return errors.New("Invalid response from API: " + resp.Status)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *LocalConfig) handleTrademeResponse(responseJson []byte) error {
|
||||||
|
var resultSet TrademeResultSet
|
||||||
|
err := json.Unmarshal(responseJson, &resultSet)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("Query complete. Result count: %d", resultSet.TotalCount)
|
||||||
|
for _, result := range resultSet.List {
|
||||||
|
c.parseTrademeListing(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update config if succcess
|
||||||
|
c.storeConfig()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *LocalConfig) parseTrademeListing(listing TradeMeListing) {
|
||||||
|
// Only send if we haven't before!
|
||||||
|
if _, ok := c.PostedProperties[listing.ListingID]; !ok {
|
||||||
|
// Send the message!
|
||||||
|
c.sendEmbeddedMessage(listing)
|
||||||
|
|
||||||
|
// Make sure we add the key in to the map so we don't send it again!
|
||||||
|
c.PostedProperties[listing.ListingID] = true
|
||||||
|
}
|
||||||
|
}
|
83
internal/flatfinder/utils.go
Normal file
83
internal/flatfinder/utils.go
Normal file
|
@ -0,0 +1,83 @@
|
||||||
|
package flatfinder
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
// storeConfig - Write current config to disk
|
||||||
|
func (c *LocalConfig) storeConfig() {
|
||||||
|
configFilePath := getConfigFilePath()
|
||||||
|
|
||||||
|
json, err := json.Marshal(c)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal("Failed to JSONify config")
|
||||||
|
}
|
||||||
|
|
||||||
|
err = os.WriteFile(configFilePath, json, 0644)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// loadConfig - Pull existing config (if exists)
|
||||||
|
func (c *LocalConfig) loadConfig() {
|
||||||
|
configFilePath := getConfigFilePath()
|
||||||
|
if fileExists(configFilePath) {
|
||||||
|
data, err := os.ReadFile(configFilePath)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load it into global
|
||||||
|
json.Unmarshal(data, c)
|
||||||
|
|
||||||
|
log.Printf("Loaded %d previously posted property IDs", len(c.PostedProperties))
|
||||||
|
} else {
|
||||||
|
maps := make(map[int64]bool)
|
||||||
|
c.PostedProperties = maps
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// getConfigFilePath - Returns a string of the config file pathg
|
||||||
|
func getConfigFilePath() string {
|
||||||
|
// path := ""
|
||||||
|
// switch runtime.GOOS {
|
||||||
|
// case "linux":
|
||||||
|
// if os.Getenv("XDG_CONFIG_HOME") != "" {
|
||||||
|
// path = os.Getenv("XDG_CONFIG_HOME")
|
||||||
|
// } else {
|
||||||
|
// path = filepath.Join(os.Getenv("HOME"), ".config")
|
||||||
|
// }
|
||||||
|
// case "windows":
|
||||||
|
// path = os.Getenv("APPDATA")
|
||||||
|
// case "darwin":
|
||||||
|
// path = os.Getenv("HOME") + "/Library/Application Support"
|
||||||
|
// default:
|
||||||
|
// log.Fatalf("Unsupported platform? %s", runtime.GOOS)
|
||||||
|
// }
|
||||||
|
|
||||||
|
// path = path + fmt.Sprintf("%c", os.PathSeparator) + "flatfinder"
|
||||||
|
// err := os.MkdirAll(path, os.ModePerm)
|
||||||
|
// if err != nil {
|
||||||
|
// log.Fatal(err)
|
||||||
|
// }
|
||||||
|
|
||||||
|
// return path + fmt.Sprintf("%c", os.PathSeparator) + "flatfinder.json"
|
||||||
|
return "flatfinder.json"
|
||||||
|
}
|
||||||
|
|
||||||
|
// fileExists - Check if a file exists
|
||||||
|
func fileExists(filePath string) bool {
|
||||||
|
if _, err := os.Stat(filePath); err == nil {
|
||||||
|
return true
|
||||||
|
} else if errors.Is(err, os.ErrNotExist) {
|
||||||
|
return false
|
||||||
|
} else {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue