-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* 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
Showing
8 changed files
with
384 additions
and
391 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 "" | ||
} |
Oops, something went wrong.