diff --git a/handlers/your-cal.go b/handlers/your-cal.go index dbb986a..556d8e6 100644 --- a/handlers/your-cal.go +++ b/handlers/your-cal.go @@ -4,13 +4,14 @@ import ( "cpe/calendar/decrypt" "cpe/calendar/ical" "cpe/calendar/request" + "fmt" "log" "net/http" "os" "strings" ) -func Health(w http.ResponseWriter, r *http.Request){ +func Health(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) } @@ -52,20 +53,14 @@ func GenerateICSHandler(w http.ResponseWriter, r *http.Request) { pass := parts[1] // Fetch data from the source - data, err := request.FetchData(start, end, username, pass) + events, err := request.FetchData(start, end, username, pass) if err != nil { log.Printf("Failed to fetch data: %v", err) http.Error(w, "Failed to fetch data", http.StatusInternalServerError) return } - // Parse the fetched data - events, err := ical.ParseEvents(data) - if err != nil { - log.Printf("Failed to parse events: %v", err) - http.Error(w, "Failed to parse events", http.StatusInternalServerError) - return - } + fmt.Println("Event found", len(events)) // Generate the iCal file with the calendar name icsContent := ical.GenerateICS(events, calendarName) @@ -79,10 +74,6 @@ func GenerateICSHandler(w http.ResponseWriter, r *http.Request) { } func ValidateHandler(w http.ResponseWriter, r *http.Request) { - // Get start and end times from environment variables - start := os.Getenv("START_TIMESTAMP") - end := os.Getenv("END_TIMESTAMP") - // Get separator from environment variable separator := os.Getenv("SEPARATOR") @@ -115,24 +106,12 @@ func ValidateHandler(w http.ResponseWriter, r *http.Request) { pass := parts[1] // Fetch data from the source - data, err := request.FetchData(start, end, username, pass) + _, err = request.Login(username, pass) if err != nil { log.Printf("Failed to fetch data: %v", err) - http.Error(w, "Failed to fetch data", http.StatusInternalServerError) - return - } - - // Parse the fetched data - events, err := ical.ParseEvents(data) - if err != nil { - log.Printf("Failed to parse events: %v", err) - http.Error(w, "Failed to parse events", http.StatusInternalServerError) + w.WriteHeader(http.StatusUnauthorized) return } - if len(events) > 0 { - w.WriteHeader(http.StatusOK) - } else { - w.WriteHeader(http.StatusUnauthorized) - } + w.WriteHeader(http.StatusOK) } diff --git a/ical/gen.go b/ical/gen.go index 38251d2..a5532b0 100644 --- a/ical/gen.go +++ b/ical/gen.go @@ -1,17 +1,13 @@ package ical import ( + "cpe/calendar/types" "fmt" - "regexp" - "strings" "time" ) -// Regular expression for splitting title into components -const regexPattern = `(?P.*?) (?P[1-9][A-Z]{3,}(?: GR[A-Z0-9])?) (?P.*?)(?P(( |n)[A-Z-]{3,} .*)|$)` - // GenerateICS generates an ICS string from a list of events -func GenerateICS(events []Event, calendarName string) string { +func GenerateICS(events []types.Event, calendarName string) string { ics := "BEGIN:VCALENDAR\n" ics += "VERSION:2.0\n" ics += "PRODID:-//github.com/qypol342 //CPE Calendar//EN\n" @@ -22,36 +18,27 @@ func GenerateICS(events []Event, calendarName string) string { ics += "REFRESH-INTERVAL;VALUE=DURATION:PT1H\n" // Define the layout for parsing the datetime with a timezone offset - const layout = "2006-01-02T15:04:05-0700" - - // Compile the regular expression - re := regexp.MustCompile(regexPattern) + const layout = "2006-01-02T15:04:05.000" for _, event := range events { - // Remove newline characters from title - cleanedTitle := strings.ReplaceAll(event.Title, "\n", " ") - // Apply regex to split title - matches := re.FindStringSubmatch(cleanedTitle) - if matches == nil { - // Handle case where regex does not match - fmt.Println("Error parsing title:", event.Title) + if event.Favori == nil { continue } // Extract components from regex matches - location := matches[1] - summary := matches[3] - description := matches[4] + location := event.Favori.F2 + summary := event.Favori.F5 + event.Favori.F3 + description := event.Favori.F4 // Parse the start and end times in the given time zone - start, err := time.Parse(layout, event.Start) + start, err := time.Parse(layout, event.DateDebut) if err != nil { // Handle parsing error fmt.Println("Error parsing start time:", err) continue } - end, err := time.Parse(layout, event.End) + end, err := time.Parse(layout, event.DateFin) if err != nil { // Handle parsing error fmt.Println("Error parsing end time:", err) @@ -64,7 +51,7 @@ func GenerateICS(events []Event, calendarName string) string { // Format times for ICS ics += "BEGIN:VEVENT\n" - ics += fmt.Sprintf("UID:%s\n", event.ID) + ics += fmt.Sprintf("UID:%d\n", event.ID) ics += fmt.Sprintf("DTSTART:%s\n", start.Format("20060102T150405Z")) ics += fmt.Sprintf("DTEND:%s\n", end.Format("20060102T150405Z")) ics += fmt.Sprintf("LOCATION:%s\n", location) diff --git a/ical/parse.go b/ical/parse.go deleted file mode 100644 index 74cc73d..0000000 --- a/ical/parse.go +++ /dev/null @@ -1,77 +0,0 @@ -package ical - -import ( - "encoding/json" - "encoding/xml" - "errors" - "fmt" - "strings" -) - -// Event struct to hold individual event data -type Event struct { - ID string `json:"id"` - Title string `json:"title"` - Start string `json:"start"` - End string `json:"end"` - AllDay bool `json:"allDay"` - Editable bool `json:"editable"` - ClassName string `json:"className"` -} - -// Define a structure to parse the XML partial-response -type PartialResponse struct { - XMLName xml.Name `xml:"partial-response"` - Changes []Update `xml:"changes>update"` -} - -// Define a structure for each update within the XML -type Update struct { - ID string `xml:"id,attr"` - CDATA string `xml:",innerxml"` -} - -// Function to parse events from the provided data -func ParseEvents(data []byte) ([]Event, error) { - var partialResponse PartialResponse - - // Parse the XML data - err := xml.Unmarshal(data, &partialResponse) - if err != nil { - return nil, err - } - - // Find the update with the event JSON data - for _, update := range partialResponse.Changes { - if strings.Contains(update.CDATA, `"events"`) { - // Extract the JSON within the CDATA section - start := strings.Index(update.CDATA, `[`) // Start of the array - end := strings.LastIndex(update.CDATA, `]`) + 1 // End of the array - if start == -1 || end == -1 { - return nil, errors.New("failed to find JSON in CDATA section") - } - jsonData := update.CDATA[start:end] - - // Remove CDATA markers if present - jsonData = strings.TrimPrefix(jsonData, "[CDATA[") - jsonData = strings.TrimSuffix(jsonData, "]]") - - // Debugging: Print extracted JSON data - fmt.Println("Extracted JSON Data:", jsonData[:150]) - - // Parse the JSON data - var events struct { - Events []Event `json:"events"` - } - err = json.Unmarshal([]byte(jsonData), &events) - if err != nil { - return nil, err - } - - // Assuming the array contains a single object with the `events` key - return events.Events, nil - } - } - - return nil, errors.New("no events found") -} diff --git a/request/request.go b/request/request.go index 5f7163c..9f41a34 100644 --- a/request/request.go +++ b/request/request.go @@ -1,28 +1,24 @@ package request import ( + "bytes" + "compress/gzip" "cpe/calendar/types" "encoding/json" - "errors" "fmt" "io" - "io/ioutil" - "log" "net/http" - "net/url" - "regexp" - "strings" + "strconv" + "time" ) -func FetchData(start, end, username, password string) ([]byte, error) { +func FetchData(start, end, username, password string) ([]types.Event, error) { - token, err := login(username, password) + token, err := Login(username, password) if err != nil { return nil, err } - log.Printf("Token: %s\n", token) - body, err := getCalendar(token, start, end) if err != nil { return nil, err @@ -31,224 +27,140 @@ func FetchData(start, end, username, password string) ([]byte, error) { return body, nil } -func login(username, password string) (types.TokenResponse, error) { +func Login(username, password string) (types.TokenResponse, error) { // Prepare the login request urlStr := "https://mycpe.cpe.fr/mobile/login" - loginData := url.Values{"login": {username}, "password": {password}} + loginData := map[string]string{ + "login": username, + "password": password, + } + + // Marshal login data to JSON + jsonData, err := json.Marshal(loginData) + if err != nil { + return types.TokenResponse{}, fmt.Errorf("failed to marshal login data: %w", err) + } // Create the request - req, err := http.NewRequest("POST", urlStr, strings.NewReader(loginData.Encode())) + req, err := http.NewRequest("POST", urlStr, bytes.NewBuffer(jsonData)) if err != nil { - return types.TokenResponse{}, err + return types.TokenResponse{}, fmt.Errorf("failed to create request: %w", err) } // Set headers req.Header.Set("User-Agent", "Dalvik/2.1.0 (Linux; U; Android 15; sdk_gphone64_x86_64 Build/AE3A.240806.005)") - req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + req.Header.Set("Content-Type", "application/json") // Send the request client := &http.Client{} resp, err := client.Do(req) if err != nil { - return types.TokenResponse{}, err + return types.TokenResponse{}, fmt.Errorf("request failed: %w", err) } defer resp.Body.Close() + // Check for non-200 status codes + if resp.StatusCode != http.StatusOK { + return types.TokenResponse{}, fmt.Errorf("received non-200 response: %d", resp.StatusCode) + } + // Read and unmarshal the response body body, err := io.ReadAll(resp.Body) if err != nil { - return types.TokenResponse{}, err + return types.TokenResponse{}, fmt.Errorf("failed to read response body: %w", err) } var formattedResp types.TokenResponse if err := json.Unmarshal(body, &formattedResp); err != nil { - return types.TokenResponse{}, err + return types.TokenResponse{}, fmt.Errorf("failed to parse JSON: %w", err) } return formattedResp, nil } +func getCalendar(token types.TokenResponse, startUnix, endUnix string) ([]types.Event, error) { + // Define the base URL and query parameters + baseURL := "https://mycpe.cpe.fr/mobile/mon_planning" - -func getUpdatedViewState(sessionCookie string) (string, error) { - // URL for the GET request - urlStr := "https://mycpe.cpe.fr/" - - // Creating the GET request - req, err := http.NewRequest("GET", urlStr, nil) + startTime, err := unixToDateTime(startUnix) if err != nil { - return "", err + return nil, fmt.Errorf("failed to parse start time: %w", err) } - // Adding headers - req.Header.Add("User-Agent", "Mozilla/5.0 (X11; Linux x86_64; rv:129.0) Gecko/20100101 Firefox/129.0") - req.Header.Add("Cookie", sessionCookie) - - // Sending the GET request - client := &http.Client{} - resp, err := client.Do(req) + endTime, err := unixToDateTime(endUnix) if err != nil { - return "", err + return nil, fmt.Errorf("failed to parse end time: %w", err) } - defer resp.Body.Close() - // Reading the response body - body, err := ioutil.ReadAll(resp.Body) - if err != nil { - return "", err - } - - // Extract the updated ViewState value from the response HTML - updatedViewState := extractViewState(string(body)) - - return updatedViewState, nil -} - -func accessMainMenu(sessionCookie, viewState string) error { - // URL for accessing MainMenuPage.xhtml - urlStr := "https://mycpe.cpe.fr/faces/MainMenuPage.xhtml" + query := fmt.Sprintf("?date_debut=%s&date_fin=%s", startTime, endTime) - // Prepare form data for the MainMenuPage request - mainMenuData := url.Values{ - "form": {"form"}, - "form:largeurDivCenter": {"827"}, - "form:idInit": {"webscolaapp.MainMenuPage_-518408921344646904"}, - "form:sauvegarde": {""}, - "javax.faces.ViewState": {viewState}, - "form:sidebar": {"form:sidebar"}, - "form:sidebar_menuid": {"8"}, - } + fmt.Println("final url ", baseURL+query) - // Creating the MainMenuPage request - req, err := http.NewRequest("POST", urlStr, strings.NewReader(mainMenuData.Encode())) + // Create the GET request + req, err := http.NewRequest("GET", baseURL+query, nil) if err != nil { - return err + return nil, fmt.Errorf("failed to create request: %w", err) } - // Adding headers - req.Header.Add("User-Agent", "Mozilla/5.0 (X11; Linux x86_64; rv:129.0) Gecko/20100101 Firefox/129.0") - req.Header.Add("Content-Type", "application/x-www-form-urlencoded") - req.Header.Add("Cookie", sessionCookie) + // Add headers to match the curl request + req.Header.Add("User-Agent", "Dalvik/2.1.0 (Linux; U; Android 15; sdk_gphone64_x86_64 Build/AE3A.240806.005)") + req.Header.Add("Authorization", "Bearer "+token.Normal) + req.Header.Add("Accept-Encoding", "gzip") + req.Header.Add("Accept-Language", "en-US,en;q=0.5") + req.Header.Add("Connection", "Keep-Alive") + req.Header.Add("Content-Type", "application/json") + req.Header.Add("Host", "mycpe.cpe.fr") - // Sending the request - client := &http.Client{ - CheckRedirect: func(req *http.Request, via []*http.Request) error { - req.Header.Add("Cookie", via[0].Header.Get("Cookie")) - return nil - }, - } + // Send the GET request + client := &http.Client{} resp, err := client.Do(req) if err != nil { - return err + return nil, fmt.Errorf("request failed: %w", err) } defer resp.Body.Close() - return nil -} - -func accessPlanningLanding(sessionCookie string) (string, string, error) { - - req, err := http.NewRequest("GET", "https://mycpe.cpe.fr/faces/Planning.xhtml", nil) - if err != nil { - return "", "", err - } - - req.Header.Set("User-Agent", "Mozilla/5.0 (X11; Linux x86_64; rv:130.0) Gecko/20100101 Firefox/130.0") - req.Header.Set("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/png,image/svg+xml,*/*;q=0.8") - req.Header.Set("Accept-Language", "en-US,en;q=0.5") - req.Header.Set("Accept-Encoding", "gzip, deflate, br, zstd") - req.Header.Set("Referer", "https://mycpe.cpe.fr/") - req.Header.Set("Connection", "keep-alive") - req.Header.Set("Cookie", sessionCookie) - req.Header.Set("Upgrade-Insecure-Requests", "1") - req.Header.Set("Sec-Fetch-Dest", "document") - req.Header.Set("Sec-Fetch-Mode", "navigate") - req.Header.Set("Sec-Fetch-Site", "same-origin") - req.Header.Set("Sec-Fetch-User", "?1") - req.Header.Set("Priority", "u=0, i") - req.Header.Set("TE", "trailers") - - resp, err := http.DefaultClient.Do(req) - if err != nil { - return "", "", err + // Handle gzip encoding if necessary + var reader io.Reader = resp.Body + if resp.Header.Get("Content-Encoding") == "gzip" { + reader, err = gzip.NewReader(resp.Body) + if err != nil { + return nil, fmt.Errorf("failed to create gzip reader: %w", err) + } + defer reader.(*gzip.Reader).Close() } - defer resp.Body.Close() - // Reading the response body - body, err := ioutil.ReadAll(resp.Body) - if err != nil { - return "", "", err + // Check for non-200 status codes + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("received non-200 response: %d", resp.StatusCode) } - // Extract the updated ViewState value from the response HTML - updatedViewState := extractViewState(string(body)) - - j_idt := extractj_idt(string(body)) - - return updatedViewState, j_idt, nil -} - -func makeFinalDataRequest(sessionCookie, viewState, j_idt, start, end string) ([]byte, error) { - // URL for the final data request - urlStr := "https://mycpe.cpe.fr/faces/Planning.xhtml" - - // Using url.Values to construct the data - data := url.Values{ - "javax.faces.partial.ajax": {"true"}, - "javax.faces.partial.render": {"form:" + j_idt}, - "form:" + j_idt: {"form:" + j_idt}, - "form:" + j_idt + "_start": {start}, - "form:" + j_idt + "_end": {end}, - "javax.faces.ViewState": {viewState}, + // Read the response body + body, err := io.ReadAll(reader) + if err != nil { + return nil, fmt.Errorf("failed to read response body: %w", err) } - log.Printf("j_idt: %s, start: %s, end: %s", j_idt, start, end) - - // Creating the request - req, err := http.NewRequest("POST", urlStr, strings.NewReader(data.Encode())) + // Parse the JSON response into the events slice + var events []types.Event + err = json.Unmarshal(body, &events) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to parse JSON: %w", err) } - // Adding headers - req.Header.Add("User-Agent", "Mozilla/5.0 (X11; Linux x86_64; rv:129.0) Gecko/20100101 Firefox/129.0") - req.Header.Add("Content-Type", "application/x-www-form-urlencoded") - req.Header.Add("Cookie", sessionCookie) + return events, nil +} - // Sending the request - client := &http.Client{} - resp, err := client.Do(req) +func unixToDateTime(rawTime string) (string, error) { + start, err := strconv.ParseInt(rawTime, 10, 64) if err != nil { - return nil, err + return "", fmt.Errorf("invalid unix time: %v", err) } - defer resp.Body.Close() - // Reading the response body - body, err := ioutil.ReadAll(resp.Body) - if err != nil { - return nil, err - } + start = start / 1000 - return body, nil -} + dateTime := time.Unix(start, 0) -func extractViewState(html string) string { - // Extract the ViewState value using a regular expression - re := regexp.MustCompile(`id="j_id1:javax.faces.ViewState:0" value="([^"]*)"`) - match := re.FindStringSubmatch(html) - if len(match) > 1 { - return match[1] - } - return "" -} + // Format the time to match the query format ("YYYY-MM-DD") + return dateTime.Format("2006-01-02"), nil -func extractj_idt(html string) string { - // Extract the j_idt value using a regular expression - // regex