Initial Commit

This commit is contained in:
idanoo 2022-09-13 19:31:25 +12:00
commit ee32ba1537
Signed by: idanoo
GPG key ID: 387387CDBC02F132
13 changed files with 583 additions and 0 deletions

View 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)
}
}

View 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()
}

View 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
}
}

View 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
}