Skip to content

Commit

Permalink
Major refactoring of the CLI (#4)
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>

---------

Signed-off-by: Aaron Sachs <898627+asachs01@users.noreply.github.com>
  • Loading branch information
asachs01 authored Sep 9, 2024
1 parent 5414a66 commit 6b87bb4
Show file tree
Hide file tree
Showing 8 changed files with 384 additions and 391 deletions.
10 changes: 5 additions & 5 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,11 @@ jobs:

- name: Build binaries
run: |
GOOS=linux GOARCH=amd64 go build -o school_menu_connector-linux-amd64
GOOS=linux GOARCH=arm64 go build -o school_menu_connector-linux-arm64
GOOS=darwin GOARCH=amd64 go build -o school_menu_connector-darwin-amd64
GOOS=darwin GOARCH=arm64 go build -o school_menu_connector-darwin-arm64
GOOS=windows GOARCH=amd64 go build -o school_menu_connector-windows-amd64.exe
GOOS=linux GOARCH=amd64 go build -o school_menu_connector-linux-amd64 ./cmd/cli
GOOS=linux GOARCH=arm64 go build -o school_menu_connector-linux-arm64 ./cmd/cli
GOOS=darwin GOARCH=amd64 go build -o school_menu_connector-darwin-amd64 ./cmd/cli
GOOS=darwin GOARCH=arm64 go build -o school_menu_connector-darwin-arm64 ./cmd/cli
GOOS=windows GOARCH=amd64 go build -o school_menu_connector-windows-amd64.exe ./cmd/cli
- name: Create Release
id: create_release
Expand Down
103 changes: 103 additions & 0 deletions cmd/cli/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
/*
School Menu Connector
Copyright (C) 2024 Aaron Sachs
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/

package main

import (
"flag"
"fmt"
"os"
"time"

"github.com/asachs01/school_menu_connector/internal/menu"
"github.com/asachs01/school_menu_connector/internal/ics"
"github.com/asachs01/school_menu_connector/internal/email"
)

func main() {
buildingID := flag.String("building", os.Getenv("BUILDING_ID"), "Building ID")
districtID := flag.String("district", os.Getenv("DISTRICT_ID"), "District ID")
recipients := flag.String("recipient", os.Getenv("RECIPIENT_EMAIL"), "Recipient email address(es), comma-separated")
sender := flag.String("sender", os.Getenv("SENDER_EMAIL"), "Sender email address")
password := flag.String("password", os.Getenv("EMAIL_PASSWORD"), "Sender email password")
smtpServer := flag.String("smtp", os.Getenv("SMTP_SERVER"), "SMTP server and port")
subject := flag.String("subject", os.Getenv("EMAIL_SUBJECT"), "Email subject line")
startDate := flag.String("startDate", os.Getenv("START_DATE"), "Start date (MM-DD-YYYY)")
endDate := flag.String("endDate", os.Getenv("END_DATE"), "End date (MM-DD-YYYY)")
weekStart := flag.String("week-start", "", "Start date of the week (MM-DD-YYYY) for calendar file")
icsOutputPath := flag.String("ics-output-path", "", "Output path for the ICS file")
emailFlag := flag.Bool("email", false, "Send email")
icsFlag := flag.Bool("ics", false, "Generate ICS file")
debugFlag := flag.Bool("debug", false, "Enable debug mode")
flag.Parse()

if err := run(*buildingID, *districtID, *recipients, *sender, *password, *smtpServer, *subject, *startDate, *endDate, *weekStart, *icsOutputPath, *emailFlag, *icsFlag, *debugFlag); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
}

func run(buildingID, districtID, recipients, sender, password, smtpServer, subject, startDate, endDate, weekStart, icsOutputPath string, emailFlag, icsFlag, debugFlag bool) error {
start, err := time.Parse("01-02-2006", startDate)
if err != nil {
return fmt.Errorf("invalid start date: %w", err)
}

end, err := time.Parse("01-02-2006", endDate)
if err != nil {
return fmt.Errorf("invalid end date: %w", err)
}

if debugFlag {
fmt.Printf("Fetching menu for date range: %s to %s\n", start.Format("01-02-2006"), end.Format("01-02-2006"))
}

menuData, err := menu.Fetch(buildingID, districtID, start.Format("01-02-2006"), end.Format("01-02-2006"), debugFlag)
if err != nil {
return fmt.Errorf("fetching menu: %w", err)
}

lunchMenu := menuData.GetLunchMenuString()
if lunchMenu == "" {
return fmt.Errorf("no lunch menu found for the specified date range")
}

if debugFlag {
fmt.Println("Lunch menu found:")
fmt.Println(lunchMenu)
}

if emailFlag {
if err := email.SendLunchMenu(buildingID, districtID, start.Format("01-02-2006"), end.Format("01-02-2006"), recipients, smtpServer, sender, password, subject, debugFlag); err != nil {
return fmt.Errorf("sending email: %w", err)
}
fmt.Println("Lunch menu sent successfully!")
}

if icsFlag {
if icsOutputPath == "" {
icsOutputPath = fmt.Sprintf("lunch_menu_%s_to_%s.ics", start.Format("01-02-2006"), end.Format("01-02-2006"))
}
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)
}

return nil
}
66 changes: 66 additions & 0 deletions internal/email/send.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package email

import (
"fmt"
"net/smtp"
"strings"

"github.com/asachs01/school_menu_connector/internal/menu"
)

// Send sends an email with the given parameters
func Send(smtpServer, from, password string, to []string, subject, body string) error {
mainRecipient := to[0]
bcc := strings.Join(to[1:], ", ")

header := make(map[string]string)
header["From"] = from
header["To"] = mainRecipient
header["Subject"] = subject
header["MIME-Version"] = "1.0"
header["Content-Type"] = "text/plain; charset=\"utf-8\""
if bcc != "" {
header["Bcc"] = bcc
}

message := ""
for k, v := range header {
message += fmt.Sprintf("%s: %s\r\n", k, v)
}
message += "\r\n" + body

auth := smtp.PlainAuth("", from, password, strings.Split(smtpServer, ":")[0])

return smtp.SendMail(smtpServer, auth, from, to, []byte(message))
}

func SendLunchMenu(buildingID, districtID, startDate, endDate string, recipients, smtpServer, sender, password, subject string, debug bool) error {
menuData, err := menu.Fetch(buildingID, districtID, startDate, endDate, debug)
if err != nil {
return fmt.Errorf("fetching menu: %w", err)
}

lunchMenu := menuData.GetLunchMenuString()
if lunchMenu == "" {
return fmt.Errorf("no lunch menu found for the specified date range")
}

recipientList := strings.Split(recipients, ",")
for i, email := range recipientList {
recipientList[i] = strings.TrimSpace(email)
}

if subject == "" {
subject = fmt.Sprintf("Lunch Menu (%s - %s)", startDate, endDate)
}

if debug {
fmt.Printf("Sending email to %s with subject: %s\n", recipients, subject)
}

if err := Send(smtpServer, sender, password, recipientList, subject, lunchMenu); err != nil {
return fmt.Errorf("sending email: %w", err)
}

return nil
}
71 changes: 71 additions & 0 deletions internal/ics/generate.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package ics

import (
"fmt"
"os"
"time"

ics "github.com/arran4/golang-ical"
"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)
if err != nil {
return fmt.Errorf("invalid start date: %w", err)
}
end, err := time.Parse("01-02-2006", endDateStr)
if err != nil {
return fmt.Errorf("invalid end date: %w", err)
}

if debug {
fmt.Printf("Creating ICS file for date range: %s to %s\n", start.Format("2006-01-02"), end.Format("2006-01-02"))
}

cal := ics.NewCalendar()
cal.SetMethod(ics.MethodPublish)

for date := start; !date.After(end); date = date.AddDate(0, 0, 1) {
dateStr := date.Format("01-02-2006")

if debug {
fmt.Printf("Fetching menu for date: %s\n", dateStr)
}

menu, err := menu.Fetch(buildingID, districtID, dateStr, dateStr, debug)
if err != nil {
if debug {
fmt.Printf("Error fetching menu for date %s: %v\n", dateStr, err)
}
continue
}

lunchMenu := menu.GetLunchMenuForDate(date.Format("1/2/2006"), debug)

if lunchMenu != "" {
event := cal.AddEvent(fmt.Sprintf("lunch-%s", date.Format("2006-01-02")))
event.SetCreatedTime(time.Now())
event.SetDtStampTime(time.Now())
event.SetModifiedAt(time.Now())
event.SetAllDayStartAt(date)
event.SetAllDayEndAt(date.AddDate(0, 0, 1))
event.SetSummary(fmt.Sprintf("Lunch Menu - %s", date.Format("01/02/2006")))
event.SetDescription(lunchMenu)

if debug {
fmt.Printf("Added event for date: %s\n", date.Format("2006-01-02"))
}
} else if debug {
fmt.Printf("No lunch menu found for date: %s\n", date.Format("01/02/2006"))
}
}

file, err := os.Create(outputPath)
if err != nil {
return fmt.Errorf("failed to create file: %w", err)
}
defer file.Close()

return cal.SerializeTo(file)
}
139 changes: 139 additions & 0 deletions internal/menu/fetch.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
package menu

import (
"encoding/json"
"fmt"
"io"
"net/http"
"strings"
)

const apiURL = "https://api.linqconnect.com/api/FamilyMenu"

// Menu represents the structure of the API response
type Menu struct {
FamilyMenuSessions []struct {
ServingSession string `json:"ServingSession"`
MenuPlans []struct {
Days []struct {
Date string `json:"Date"`
MenuMeals []struct {
MenuMealName string `json:"MenuMealName"`
RecipeCategories []struct {
CategoryName string `json:"CategoryName"`
Recipes []struct {
RecipeName string `json:"RecipeName"`
} `json:"Recipes"`
} `json:"RecipeCategories"`
} `json:"MenuMeals"`
} `json:"Days"`
} `json:"MenuPlans"`
} `json:"FamilyMenuSessions"`
AcademicCalendars []interface{} `json:"AcademicCalendars"`
}

// Fetch retrieves the menu from the API for the given building, district, and date range
func Fetch(buildingID, districtID, startDate, endDate string, debug bool) (*Menu, error) {
url := constructURL(buildingID, districtID, startDate, endDate)
if debug {
fmt.Printf("API URL: %s\n", url)
}

resp, err := http.Get(url)
if err != nil {
return nil, fmt.Errorf("HTTP GET request failed: %w", err)
}
defer resp.Body.Close()

body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("reading response body: %w", err)
}

if debug {
fmt.Printf("Response body: %s\n", string(body))
}

var menu Menu
if err := json.Unmarshal(body, &menu); err != nil {
return nil, fmt.Errorf("unmarshaling JSON: %w", err)
}

if debug {
fmt.Printf("Parsed menu: %+v\n", menu)
}

return &menu, nil
}

// constructURL builds the API URL with the given parameters
func constructURL(buildingID, districtID, startDate, endDate string) string {
return fmt.Sprintf("%s?buildingId=%s&districtId=%s&startDate=%s&endDate=%s", apiURL, buildingID, districtID, startDate, endDate)
}

func (m *Menu) GetLunchMenuString() string {
var lunchMenu strings.Builder

for _, session := range m.FamilyMenuSessions {
if session.ServingSession == "Lunch" {
for _, plan := range session.MenuPlans {
for _, day := range plan.Days {
fmt.Fprintf(&lunchMenu, "Lunch Menu for %s:\n\n", day.Date)
for _, meal := range day.MenuMeals {
fmt.Fprintf(&lunchMenu, "%s:\n", meal.MenuMealName)
for _, category := range meal.RecipeCategories {
fmt.Fprintf(&lunchMenu, " %s:\n", category.CategoryName)
for _, recipe := range category.Recipes {
fmt.Fprintf(&lunchMenu, " - %s\n", recipe.RecipeName)
}
}
lunchMenu.WriteString("\n")
}
lunchMenu.WriteString("\n")
}
}
return lunchMenu.String()
}
}
return ""
}

func (m *Menu) GetLunchMenuForDate(date string, debug bool) string {
var lunchMenu strings.Builder

if debug {
fmt.Printf("Searching for lunch menu on date: %s\n", date)
}

for _, session := range m.FamilyMenuSessions {
if session.ServingSession == "Lunch" {
for _, plan := range session.MenuPlans {
for _, day := range plan.Days {
if debug {
fmt.Printf("Checking day: %s\n", day.Date)
}

if day.Date == date {
fmt.Fprintf(&lunchMenu, "Lunch Menu for %s:\n\n", day.Date)
for _, meal := range day.MenuMeals {
fmt.Fprintf(&lunchMenu, "%s:\n", meal.MenuMealName)
for _, category := range meal.RecipeCategories {
fmt.Fprintf(&lunchMenu, " %s:\n", category.CategoryName)
for _, recipe := range category.Recipes {
fmt.Fprintf(&lunchMenu, " - %s\n", recipe.RecipeName)
}
}
lunchMenu.WriteString("\n")
}
return lunchMenu.String()
}
}
}
}
}

if debug {
fmt.Printf("No lunch menu found for date: %s\n", date)
}
return ""
}
Loading

0 comments on commit 6b87bb4

Please sign in to comment.