diff --git a/.env.dist b/.env.dist new file mode 100644 index 0000000..173e957 --- /dev/null +++ b/.env.dist @@ -0,0 +1,2 @@ +export WOOS_KEY=woos-abcdefghijklmnop0123456789 +export WOOS_DOMAIN=mondomaine.fr \ No newline at end of file diff --git a/.gitignore b/.gitignore index 839a56d..81a8163 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,5 @@ # Dependency directories (remove the comment below to include it) vendor/ + +.env \ No newline at end of file diff --git a/README.md b/README.md index 1b6bbc2..8aa3fc0 100644 --- a/README.md +++ b/README.md @@ -4,10 +4,11 @@ Ce script a pour objet la recherche de créneau dans les Drives Auchan. ## Usage -Pour l'identifiant de votre drive, on peut le trouver -dans les liens présent sur cette page : [Liste des drives](https://www.auchandrive.fr/drive/nos-drives/) +Pour l'identifiant de votre drive, on peut le trouver dans les liens présent sur cette page : [Liste des drives](https://www.auchandrive.fr/drive/nos-drives/) -Le script va tourné en continue et va affiché sur la console si un créneau est disponible +Si vous possédez une clé woosmap avec tous les drives vous pouvez renseigner ces informations dans un fichier .env (voir .env.dist) + +Le script va tourner en continue et va afficher sur la console si un créneau est disponible Pour lancer le scripts : @@ -15,14 +16,34 @@ Pour lancer le scripts : ./go-auchan-drive-checker -id [ID DU DRIVE] ``` +Ou si vous avez une clé woosmap, vous pouvez utiliser la recherche par code postal : + +```bash +./go-auchan-drive-checker -cp [CODE POSTAL] +``` + +## Usage API + Pour rendre accessible la recherche de créneau via une mini API : ```bash -./go-auchan-drive-checker -id [ID DU DRIVE] -port 8089 -host 0.0.0.0 +./go-auchan-drive-checker -port 8089 -host 0.0.0.0 & +``` + +Pour avoir la liste des clés de drive disponible : + +```bash +curl 127.0.0.1:8089/stores?postalCode=[CODE POSTAL] +``` + +Pour ajouter un scrapper sur un store : + +```bash +curl -XPUT 127.0.0.1:8089/scrappers/[ID DU DRIVE] ``` -Pour tester le serveur : +Pour checké l'état d'un drive : ```bash -curl 127.0.0.1:8089/ +curl 127.0.0.1:8089/scrappers/[ID DU DRIVE] ``` diff --git a/internal/api/main.go b/internal/api/main.go new file mode 100644 index 0000000..470c55d --- /dev/null +++ b/internal/api/main.go @@ -0,0 +1,16 @@ +package api + +import ( + "log" + "net/http" + + "github.com/gorilla/mux" +) + +// StartServer demarrage du server +func StartServer(host string, port string) { + r := mux.NewRouter() + addStoreRoutes(r) + addScrapperRoutes(r) + log.Fatal(http.ListenAndServe(host+":"+port, r)) +} \ No newline at end of file diff --git a/internal/api/scrappers.go b/internal/api/scrappers.go new file mode 100644 index 0000000..2d505dd --- /dev/null +++ b/internal/api/scrappers.go @@ -0,0 +1,40 @@ +package api + +import ( + "encoding/json" + "log" + "net/http" + + "github.com/gorilla/mux" + "github.com/nlevee/go-auchan-drive-checker/pkg/auchan" +) + +// AddScrapperRoutes populate router +func addScrapperRoutes(r *mux.Router) { + // ajoute un scrapper sur le store : storeid + r.HandleFunc("/scrappers/{storeid}", addScrapper).Methods(http.MethodPut) + // récupère l'état du scrapper sur le store : storeid + r.HandleFunc("/scrappers/{storeid}", getScrapperState).Methods(http.MethodGet) +} + +// GetScrapperState récuperation d'un état de scrapper +func getScrapperState(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + vars := mux.Vars(r) + log.Printf("storeId: %v\n", vars["storeid"]) + + json.NewEncoder(w).Encode(auchan.GetDriveState(vars["storeid"])) +} + +// AddScrapper ajoute un scrapper +func addScrapper(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + + vars := mux.Vars(r) + log.Printf("storeId: %v\n", vars["storeid"]) + + go auchan.NewDriveHandler(vars["storeid"]) +} diff --git a/internal/api/stores.go b/internal/api/stores.go new file mode 100644 index 0000000..7d0a54a --- /dev/null +++ b/internal/api/stores.go @@ -0,0 +1,32 @@ +package api + +import ( + "encoding/json" + "net/http" + + "github.com/gorilla/mux" + "github.com/nlevee/go-auchan-drive-checker/pkg/auchan" +) + +// AddStoreRoutes populate router +func addStoreRoutes(r *mux.Router) { + // récuperation des stores + r.HandleFunc("/stores", getStores).Methods(http.MethodGet) +} + +// GetStores récupere la liste des stores filtrés +func getStores(w http.ResponseWriter, r *http.Request) { + params := r.URL.Query() + cp := params["postalCode"] + if len(cp) == 0 { + w.WriteHeader(http.StatusBadRequest) + return + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + storeIDs, _ := auchan.GetStoreByPostalCode(string(cp[0])) + + json.NewEncoder(w).Encode(storeIDs) +} diff --git a/main.go b/main.go index 658071d..4ead2f9 100644 --- a/main.go +++ b/main.go @@ -3,54 +3,39 @@ package main import ( "flag" "log" - "net/http" "os" - "time" - "github.com/gorilla/mux" + "github.com/nlevee/go-auchan-drive-checker/internal/api" "github.com/nlevee/go-auchan-drive-checker/pkg/auchan" - "github.com/nlevee/go-auchan-drive-checker/pkg/drivestate" ) -func handle(currentState *drivestate.DriveState) func(w http.ResponseWriter, r *http.Request) { - return func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusOK) - if _, err := w.Write([]byte(`{"message": "` + (*currentState).Dispo + `"}`)); err != nil { - log.Fatal(err) - } - log.Printf("current state is : %v", (*currentState).IsActive) - } -} - -var tick = time.NewTicker(2 * time.Minute) -var done = make(chan bool) -var currentState = make(map[string]*drivestate.DriveState) - -func addDriveHandler(driveId string) { - config := auchan.NewConfig(driveId) - currentState[driveId] = config.State - go auchan.GetDriveState(config, tick, done) -} - func main() { - auchanDriveId := flag.String("id", "", "The drive Id") + auchanDriveID := flag.String("id", "", "The drive Id") + postalCode := flag.String("cp", "", "The Postal Code") listenHost := flag.String("host", "0.0.0.0", "Start a server and listen on this host") listenPort := flag.String("port", "", "Start a server and listen on this port") flag.Parse() - if *auchanDriveId == "" { - flag.PrintDefaults() - os.Exit(1) + // recherche du driveId si code postal + if *auchanDriveID == "" && *postalCode != "" { + storeIDs, _ := auchan.GetStoreIDByPostalCode(*postalCode) + if len(storeIDs) > 0 { + auchanDriveID = &storeIDs[0] + } else { + log.Fatal("no stores found") + } } if *listenPort != "" && *listenHost != "" { - addDriveHandler(*auchanDriveId) - r := mux.NewRouter() - r.HandleFunc("/", handle(currentState[*auchanDriveId])).Methods(http.MethodGet) - log.Fatal(http.ListenAndServe(*listenHost+":"+*listenPort, r)) + if *auchanDriveID != "" { + go auchan.NewDriveHandler(*auchanDriveID) + } + api.StartServer(*listenHost, *listenPort) } else { - config := auchan.NewConfig(*auchanDriveId) - auchan.GetDriveState(config, tick, done) + if *auchanDriveID == "" { + flag.PrintDefaults() + os.Exit(1) + } + auchan.NewDriveHandler(*auchanDriveID) } } diff --git a/pkg/auchan/main.go b/pkg/auchan/main.go index 35cfcef..4b81935 100644 --- a/pkg/auchan/main.go +++ b/pkg/auchan/main.go @@ -2,7 +2,14 @@ package auchan import ( + "bytes" + "encoding/json" + "fmt" + "io/ioutil" "log" + "net/http" + "net/http/httputil" + "os" "time" "github.com/antchfx/htmlquery" @@ -11,33 +18,117 @@ import ( ) const ( - auchanDriveUrl = "https://www.auchandrive.fr/drive/mag/anything-" + auchanDriveURL = "https://www.auchandrive.fr/drive/mag/anything-" ) type DriveConfig struct { - DriveId string + DriveID string State *drivestate.DriveState } -// Create a new Drive config with driveId -func NewConfig(driveId string) DriveConfig { +type DriveStore struct { + DriveID string + Name string +} + +// NewConfig Create a new Drive config with driveId +func NewConfig(driveID string) DriveConfig { state := &drivestate.DriveState{ IsActive: false, Dispo: "", } return DriveConfig{ - DriveId: driveId, + DriveID: driveID, State: state, } } +type store struct { + Features []struct { + Properties struct { + Store_ID string + Name string + } + } +} + +// GetStoreByPostalCode fetch stores by postal code +func GetStoreByPostalCode(postalCode string) ([]DriveStore, error) { + stores := []DriveStore{} + + cities, err := utils.GetCitiesByPostalCode(postalCode) + if err != nil || len(cities) == 0 { + return stores, err + } + + woosKey := os.Getenv("WOOS_KEY") + woosDomain := os.Getenv("WOOS_DOMAIN") + if woosKey == "" || woosDomain == "" { + log.Fatal("env var 'WOOS_KEY' and 'WOOS_DOMAIN' are required") + } + url := "https://api.woosmap.com/stores/search?key=" + woosKey + "&max_distance=20000&query=type:DRIVE" + + city := cities[0] + + requrl := url + "&lat=" + fmt.Sprintf("%f", city.Lat) + "&lng=" + fmt.Sprintf("%f", city.Lon) + log.Print(requrl) + req, err := http.NewRequest("GET", requrl, bytes.NewReader([]byte{})) + if err != nil { + log.Print(err) + return stores, err + } + req.Header.Add("origin", "https://"+os.Getenv("WOOS_DOMAIN")) + resp, err := http.DefaultClient.Do(req) + if err != nil || resp.StatusCode != 200 { + dump, _ := httputil.DumpRequestOut(req, true) + log.Println(err, resp.Status, string(dump)) + return stores, err + } + + defer resp.Body.Close() + + bodyContent, err := ioutil.ReadAll(resp.Body) + if err != nil { + log.Print(err) + return stores, err + } + + storeFound := store{} + json.Unmarshal(bodyContent, &storeFound) + + for _, v := range storeFound.Features { + stores = append(stores, DriveStore{ + DriveID: v.Properties.Store_ID, + Name: v.Properties.Name, + }) + } + + return stores, nil +} + +// GetStoreIDByPostalCode fetch storeIDs by postal code +func GetStoreIDByPostalCode(postalCode string) ([]string, error) { + storeIds := []string{} + + stores, err := GetStoreByPostalCode(postalCode) + if err != nil { + return storeIds, err + } + + for _, v := range stores { + storeIds = append(storeIds, v.DriveID) + } + + return storeIds, nil +} + func loadDriveState(config DriveConfig) (hasChanged bool, err error) { - driveUrl := auchanDriveUrl + config.DriveId + driveURL := auchanDriveURL + config.DriveID currentState := config.State - log.Printf("Request uri : %v", driveUrl) + log.Printf("Request uri : %v", driveURL) - doc, err := utils.LoadURL(driveUrl) + doc, err := utils.LoadHTMLURL(driveURL) if err != nil { return false, err } @@ -50,16 +141,18 @@ func loadDriveState(config DriveConfig) (hasChanged bool, err error) { return true, nil } } else if (*currentState).IsActive { - (*currentState).IsActive = false log.Printf("Aucun créneau pour le moment") + (*currentState).IsActive = false return true, nil + } else { + log.Printf("Aucun créneau pour le moment") } return false, nil } -func GetDriveState(config DriveConfig, tick *time.Ticker, done chan bool) { - - log.Printf("Démarrage du check de créneau Auchan Drive %v", config.DriveId) +// LoadIntervalDriveState fetch each tick the drive state config +func LoadIntervalDriveState(config DriveConfig, tick *time.Ticker, done chan bool) { + log.Printf("Démarrage du check de créneau Auchan Drive %v", config.DriveID) // premier appel sans attendre le premier tick if _, err := loadDriveState(config); err != nil { @@ -80,3 +173,19 @@ func GetDriveState(config DriveConfig, tick *time.Ticker, done chan bool) { } } } + +// GetDriveState get the state of a drive +func GetDriveState(driveID string) *drivestate.DriveState { + return drivestate.GetDriveState(driveID) +} + +// NewDriveHandler add a new drive handler +func NewDriveHandler(driveID string) { + config := NewConfig(driveID) + drivestate.NewDriveState(driveID, config.State) + + tick := time.NewTicker(2 * time.Minute) + done := make(chan bool) + + LoadIntervalDriveState(config, tick, done) +} diff --git a/pkg/drivestate/drivestate.go b/pkg/drivestate/drivestate.go deleted file mode 100644 index 433796e..0000000 --- a/pkg/drivestate/drivestate.go +++ /dev/null @@ -1,6 +0,0 @@ -package drivestate - -type DriveState struct { - IsActive bool - Dispo string -} diff --git a/pkg/drivestate/main.go b/pkg/drivestate/main.go new file mode 100644 index 0000000..de78534 --- /dev/null +++ b/pkg/drivestate/main.go @@ -0,0 +1,21 @@ +package drivestate + +// DriveState struct +type DriveState struct { + IsActive bool + Dispo string +} + +type state map[string]*DriveState + +var currentState = make(state) + +// GetDriveState get the state of a drive +func GetDriveState(driveID string) *DriveState { + return currentState[driveID] +} + +// NewDriveState create a new store +func NewDriveState(driveID string, state *DriveState) { + currentState[driveID] = state +} diff --git a/pkg/utils/geo.go b/pkg/utils/geo.go new file mode 100644 index 0000000..06f0cf6 --- /dev/null +++ b/pkg/utils/geo.go @@ -0,0 +1,51 @@ +package utils + +import ( + "encoding/json" + "io/ioutil" + "log" + "net/http" +) + +// Cities expose a city structure +type Cities struct { + Name string + Lat float64 + Lon float64 +} + +type communes []struct { + Code string + Nom string + Centre struct { + Coordinates []float64 + } +} + +// GetCitiesByPostalCode fetch cities center point by postal code +func GetCitiesByPostalCode(postalCode string) ([]Cities, error) { + cities := []Cities{} + + url := "https://geo.api.gouv.fr/communes?codePostal=" + postalCode + "&fields=centre" + + resp, err := http.Get(url) + if err != nil { + log.Print(err) + return cities, err + } + defer resp.Body.Close() + + bodyContent, _ := ioutil.ReadAll(resp.Body) + var foundCities communes + json.Unmarshal(bodyContent, &foundCities) + + for _, v := range foundCities { + cities = append(cities, Cities{ + Name: v.Nom, + Lon: v.Centre.Coordinates[0], + Lat: v.Centre.Coordinates[1], + }) + } + + return cities, nil +} diff --git a/pkg/utils/loadurl.go b/pkg/utils/loadurl.go index 21b9767..8d8851b 100644 --- a/pkg/utils/loadurl.go +++ b/pkg/utils/loadurl.go @@ -9,8 +9,8 @@ import ( "golang.org/x/net/publicsuffix" ) -// Override LoadURL to keep cookie with redirect -func LoadURL(url string) (*html.Node, error) { +// LoadHTMLURL to keep cookie with redirect +func LoadHTMLURL(url string) (*html.Node, error) { options := cookiejar.Options{ PublicSuffixList: publicsuffix.List, }