Skip to content

Commit

Permalink
Refactoring to make CLI & web separate (#5)
Browse files Browse the repository at this point in the history
* Major restructuring

Signed-off-by: Aaron Sachs <898627+asachs01@users.noreply.github.com>

* Major restructuring

Signed-off-by: Aaron Sachs <898627+asachs01@users.noreply.github.com>

* Major refactor to split the CLI into its own tool

Signed-off-by: Aaron Sachs <898627+asachs01@users.noreply.github.com>

* Continuing to refactor the webUI

Signed-off-by: Aaron Sachs <898627+asachs01@users.noreply.github.com>

* Refactored to allow the web utility to live on its own

Signed-off-by: Aaron Sachs <898627+asachs01@users.noreply.github.com>

---------

Signed-off-by: Aaron Sachs <898627+asachs01@users.noreply.github.com>
  • Loading branch information
asachs01 authored Sep 9, 2024
1 parent 6b87bb4 commit ae12a8f
Show file tree
Hide file tree
Showing 8 changed files with 493 additions and 16 deletions.
11 changes: 5 additions & 6 deletions cmd/cli/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,13 +90,12 @@ func run(buildingID, districtID, recipients, sender, password, smtpServer, subje
}

if icsFlag {
if icsOutputPath == "" {
icsOutputPath = fmt.Sprintf("lunch_menu_%s_to_%s.ics", start.Format("01-02-2006"), end.Format("01-02-2006"))
outputPath := fmt.Sprintf("lunch_menu_%s_to_%s.ics", start.Format("01-02-2006"), end.Format("01-02-2006"))
_, err := ics.GenerateICSFile(buildingID, districtID, startDate, endDate, outputPath, debugFlag)
if err != nil {
return fmt.Errorf("failed to generate ICS file: %v", err)
}
if err := ics.GenerateICSFile(buildingID, districtID, start.Format("01-02-2006"), end.Format("01-02-2006"), icsOutputPath, debugFlag); err != nil {
return fmt.Errorf("creating ICS file: %w", err)
}
fmt.Printf("ICS file created at: %s\n", icsOutputPath)
fmt.Printf("ICS file generated successfully: %s\n", outputPath)
}

return nil
Expand Down
225 changes: 225 additions & 0 deletions cmd/web/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
package main

import (
"encoding/json"
"fmt"
"log"
"net/http"
"os"
"runtime/debug"
"strings"

"github.com/asachs01/school_menu_connector/internal/menu"
"github.com/asachs01/school_menu_connector/internal/ics"
mailjet "github.com/mailjet/mailjet-apiv3-go/v4"
)

// Create a custom logger
var (
infoLog = log.New(os.Stdout, "INFO\t", log.Ldate|log.Ltime)
errorLog = log.New(os.Stderr, "ERROR\t", log.Ldate|log.Ltime|log.Lshortfile)
)

var senderEmail string

func init() {
senderEmail = os.Getenv("SENDER_EMAIL")
if senderEmail == "" {
senderEmail = "noreply@schoolmenuconnector.com"
}
}

func main() {
// Serve static files
fs := http.FileServer(http.Dir("./web"))
http.Handle("/", fs)

// API endpoint
http.HandleFunc("/api/generate", logRequest(handleGenerate))

infoLog.Println("Starting server on :8080")
err := http.ListenAndServe(":8080", nil)
errorLog.Fatal(err)
}

func logRequest(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
infoLog.Printf("%s - %s %s %s", r.RemoteAddr, r.Proto, r.Method, r.URL.RequestURI())
next.ServeHTTP(w, r)
}
}

func handleGenerate(w http.ResponseWriter, r *http.Request) {
defer func() {
if r := recover(); r != nil {
errorLog.Printf("panic: %v\n%s", r, debug.Stack())
http.Error(w, "Internal server error", http.StatusInternalServerError)
}
}()

if r.Method != http.MethodPost {
errorLog.Printf("Method not allowed: %s", r.Method)
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}

var data map[string]interface{}
if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
errorLog.Printf("Invalid request body: %v", err)
http.Error(w, "Invalid request body", http.StatusBadRequest)
return
}

buildingID, ok := data["buildingId"].(string)
if !ok {
errorLog.Print("Missing or invalid buildingId")
http.Error(w, "Missing or invalid buildingId", http.StatusBadRequest)
return
}

districtID, ok := data["districtId"].(string)
if !ok {
errorLog.Print("Missing or invalid districtId")
http.Error(w, "Missing or invalid districtId", http.StatusBadRequest)
return
}

startDate, ok := data["startDate"].(string)
if !ok {
errorLog.Print("Missing or invalid startDate")
http.Error(w, "Missing or invalid startDate", http.StatusBadRequest)
return
}

endDate, ok := data["endDate"].(string)
if !ok {
errorLog.Print("Missing or invalid endDate")
http.Error(w, "Missing or invalid endDate", http.StatusBadRequest)
return
}

action, ok := data["action"].(string)
if !ok {
errorLog.Print("Missing or invalid action")
http.Error(w, "Missing or invalid action", http.StatusBadRequest)
return
}

menuData, err := menu.Fetch(buildingID, districtID, startDate, endDate, false)
if err != nil {
errorLog.Printf("Error fetching menu: %v", err)
http.Error(w, fmt.Sprintf("Error fetching menu: %v", err), http.StatusInternalServerError)
return
}

switch action {
case "email":
message, err := handleEmail(data, menuData)
if err != nil {
errorLog.Printf("Error handling email: %v", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
response := map[string]string{"message": message}
w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(response); err != nil {
errorLog.Printf("Error encoding response: %v", err)
http.Error(w, "Error encoding response", http.StatusInternalServerError)
}
case "ics":
if err := handleICS(w, data, menuData); err != nil {
errorLog.Printf("Error handling ICS: %v", err)
http.Error(w, fmt.Sprintf("Error generating calendar file: %v", err), http.StatusInternalServerError)
}
// Don't write anything else here
default:
errorLog.Printf("Invalid action: %s", action)
http.Error(w, "Invalid action", http.StatusBadRequest)
}
}

func handleEmail(data map[string]interface{}, menuData *menu.Menu) (string, error) {
recipients, ok := data["recipients"].(string)
if !ok || recipients == "" {
return "", fmt.Errorf("missing or invalid recipients")
}

recipientList := strings.Split(recipients, ",")

mailjetClient := mailjet.NewMailjetClient(os.Getenv("MJ_APIKEY_PUBLIC"), os.Getenv("MJ_APIKEY_PRIVATE"))

var recipientsV31 mailjet.RecipientsV31
for _, recipient := range recipientList {
recipientsV31 = append(recipientsV31, mailjet.RecipientV31{
Email: strings.TrimSpace(recipient),
})
}

messagesInfo := []mailjet.InfoMessagesV31{
{
From: &mailjet.RecipientV31{
Email: senderEmail,
Name: "School Menu Connector",
},
To: &recipientsV31,
Subject: "School Menu",
TextPart: menuData.GetLunchMenuString(),
HTMLPart: "<h3>School Menu</h3><pre>" + menuData.GetLunchMenuString() + "</pre>",
},
}

messages := mailjet.MessagesV31{Info: messagesInfo}
res, err := mailjetClient.SendMailV31(&messages)
if err != nil {
errorLog.Printf("Mailjet API error: %v", err)
return "", fmt.Errorf("failed to send email: %v", err)
}

// Log the Mailjet response
infoLog.Printf("Mailjet response: %+v", res)

return "Email sent successfully", nil
}

func handleICS(w http.ResponseWriter, data map[string]interface{}, menuData *menu.Menu) error {
buildingID := data["buildingId"].(string)
districtID := data["districtId"].(string)
startDate := data["startDate"].(string)
endDate := data["endDate"].(string)

infoLog.Printf("Generating ICS file for buildingID: %s, districtID: %s, startDate: %s, endDate: %s",
buildingID, districtID, startDate, endDate)

icsContent, err := ics.GenerateICSFile(buildingID, districtID, startDate, endDate, "", false)
if err != nil {
errorLog.Printf("Failed to generate ICS file: %v", err)
return fmt.Errorf("failed to generate ICS file: %w", err)
}

infoLog.Printf("ICS file generated successfully, content length: %d bytes", len(icsContent))

filename := fmt.Sprintf("lunch_menu_%s_to_%s.ics", startDate, endDate)

infoLog.Printf("Setting headers: Content-Type: %s, Content-Disposition: %s, Content-Length: %d",
"text/calendar", fmt.Sprintf("attachment; filename=\"%s\"", filename), len(icsContent))

w.Header().Set("Content-Type", "text/calendar")
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", filename))
w.Header().Set("Content-Length", fmt.Sprintf("%d", len(icsContent)))

n, err := w.Write(icsContent)
if err != nil {
errorLog.Printf("Error writing ICS content to response: %v", err)
return fmt.Errorf("error writing ICS content to response: %w", err)
}
infoLog.Printf("Wrote %d bytes to response", n)

// Log the first 100 characters of the ICS content
if len(icsContent) > 100 {
infoLog.Printf("First 100 characters of ICS content: %s", string(icsContent[:100]))
} else {
infoLog.Printf("ICS content: %s", string(icsContent))
}

return nil
}
5 changes: 5 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,8 @@ module github.com/asachs01/school_menu_connector
go 1.21.5

require github.com/arran4/golang-ical v0.3.1

require (
github.com/mailjet/mailjet-apiv3-go/v3 v3.2.0 // indirect
github.com/mailjet/mailjet-apiv3-go/v4 v4.0.1
)
4 changes: 4 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeN
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/mailjet/mailjet-apiv3-go/v3 v3.2.0 h1:/gjowTurgK4iqLzVAQmjtcldyaW6tbJNA4PzZsuj2Ks=
github.com/mailjet/mailjet-apiv3-go/v3 v3.2.0/go.mod h1:Nw3mVzRxV0CVDTlzaRcADGKt4PMNbT7gYIyEtjMrVIM=
github.com/mailjet/mailjet-apiv3-go/v4 v4.0.1 h1:VwdxYT1lPOIBZolqNtN6GcpdOySgHhCFQNsbN5P7uh8=
github.com/mailjet/mailjet-apiv3-go/v4 v4.0.1/go.mod h1:2SU3t6eh/uK6BSeBmdhpIUau99L4iPlIfbx4o4pAUQs=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
Expand Down
25 changes: 15 additions & 10 deletions internal/ics/generate.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,14 @@ import (
"github.com/asachs01/school_menu_connector/internal/menu"
)

func GenerateICSFile(buildingID, districtID, startDateStr, endDateStr, outputPath string, debug bool) error {
start, err := time.Parse("01-02-2006", startDateStr)
func GenerateICSFile(buildingID, districtID, startDate, endDate string, outputPath string, debug bool) ([]byte, error) {
start, err := time.Parse("01-02-2006", startDate)
if err != nil {
return fmt.Errorf("invalid start date: %w", err)
return nil, fmt.Errorf("invalid start date: %w", err)
}
end, err := time.Parse("01-02-2006", endDateStr)
end, err := time.Parse("01-02-2006", endDate)
if err != nil {
return fmt.Errorf("invalid end date: %w", err)
return nil, fmt.Errorf("invalid end date: %w", err)
}

if debug {
Expand Down Expand Up @@ -61,11 +61,16 @@ func GenerateICSFile(buildingID, districtID, startDateStr, endDateStr, outputPat
}
}

file, err := os.Create(outputPath)
if err != nil {
return fmt.Errorf("failed to create file: %w", err)
icsContent := cal.Serialize()
icsBytes := []byte(icsContent)

// If outputPath is provided, write to file (for CLI use)
if outputPath != "" {
err := os.WriteFile(outputPath, icsBytes, 0644)
if err != nil {
return nil, fmt.Errorf("failed to write ICS file: %w", err)
}
}
defer file.Close()

return cal.SerializeTo(file)
return icsBytes, nil
}
Loading

0 comments on commit ae12a8f

Please sign in to comment.