diff --git a/.air.toml b/.air.toml new file mode 100644 index 0000000..9ce72dc --- /dev/null +++ b/.air.toml @@ -0,0 +1,46 @@ +root = "." +testdata_dir = "testdata" +tmp_dir = "tmp" + +[build] + args_bin = [] + bin = "areweplaying" + cmd = "make build" + delay = 1000 + exclude_dir = ["assets", "tmp", "vendor", "testdata"] + exclude_file = [] + exclude_regex = [".*_templ.go", "_test.go"] + exclude_unchanged = false + follow_symlink = false + full_bin = "" + include_dir = [] + include_ext = ["go", "tpl", "tmpl", "templ", "html"] + include_file = [] + kill_delay = "0s" + log = "build-errors.log" + poll = false + poll_interval = 0 + post_cmd = [] + pre_cmd = [] + rerun = false + rerun_delay = 500 + send_interrupt = false + stop_on_error = false + +[color] + app = "" + build = "yellow" + main = "magenta" + runner = "green" + watcher = "cyan" + +[log] + main_only = false + time = false + +[misc] + clean_on_exit = false + +[screen] + clear_on_rebuild = false + keep_scroll = true diff --git a/.gitignore b/.gitignore index 3b735ec..3221188 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,16 @@ *.dll *.so *.dylib +.env +areweplaying +areweplaying.* +awp-cli +awp-cli.* +players.json +.DS_Store + +# air-related stuff +tmp # Test binary, built with `go test -c` *.test diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..9066a1e --- /dev/null +++ b/Makefile @@ -0,0 +1,17 @@ +all: build + +build: + templ generate + go build -o areweplaying cmd/web/*.go + go build -o awp-cli cmd/cli/*.go + +linuxbuild: + templ generate + GOOS=linux GOARCH=amd64 go build -o areweplaying.linux cmd/web/*.go + GOOS=linux GOARCH=amd64 go build -o awp-cli.linux cmd/cli/*.go + +clean: + rm -rf areweplaying + rm -rf areweplaying.* + +test: diff --git a/cmd/cli/cmd/check.go b/cmd/cli/cmd/check.go new file mode 100644 index 0000000..f0cfd56 --- /dev/null +++ b/cmd/cli/cmd/check.go @@ -0,0 +1,82 @@ +/* +Copyright © 2024 NAME HERE +*/ +package cmd + +import ( + "log" + "strings" + + "github.com/hculpan/areweplaying/pkg/data" + "github.com/hculpan/areweplaying/pkg/email" + "github.com/spf13/cobra" +) + +func isAttending(body string) string { + body = strings.ToLower(body) + lines := strings.Fields(body) + if strings.Contains(lines[0], "yes") { + return "yes" + } else if strings.Contains(lines[0], "no") { + return "no" + } else { + return "unknown" + } +} + +// checkCmd represents the check command +var checkCmd = &cobra.Command{ + Use: "check", + Short: "Checks for new emails on player status", + Long: `Checks at culpanvtt@gmail.com for any new +emails sent by players to indicate if they +will attend the next session.`, + RunE: func(cmd *cobra.Command, args []string) error { + log.Default().Println("Checking for emails") + emails, err := email.FetchEmails() + if err != nil { + return err + } + + if len(emails) == 0 { + log.Default().Println("No new emails") + } else { + session, err := data.ReadSessionData() + if err != nil { + log.Default().Println(err) + return err + } + + for _, email := range emails { + if len(email.Body) > 0 { + player := session.GetPlayerByEmail(email.FromAddress) + if player != nil { + attending := isAttending(email.Body) + if attending == "yes" || attending == "no" { + player.Attending = attending + log.Default().Printf("Received player response for %s: %s", player.Name, player.Attending) + } + } + } + } + + data.PersistSession(session) + } + + return nil + }, +} + +func init() { + rootCmd.AddCommand(checkCmd) + + // Here you will define your flags and configuration settings. + + // Cobra supports Persistent Flags which will work for this command + // and all subcommands, e.g.: + // checkCmd.PersistentFlags().String("foo", "", "A help for foo") + + // Cobra supports local flags which will only run when this command + // is called directly, e.g.: + // checkCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") +} diff --git a/cmd/cli/cmd/root.go b/cmd/cli/cmd/root.go new file mode 100644 index 0000000..39b1afa --- /dev/null +++ b/cmd/cli/cmd/root.go @@ -0,0 +1,42 @@ +/* +Copyright © 2024 NAME HERE +*/ +package cmd + +import ( + "os" + + "github.com/spf13/cobra" +) + +// rootCmd represents the base command when called without any subcommands +var rootCmd = &cobra.Command{ + Use: "awp-cli", + Short: "CLI utility for Are We Playing? web app", + Long: `CLI utility for Are We Playing? web app`, + // Uncomment the following line if your bare application + // has an action associated with it: + // Run: func(cmd *cobra.Command, args []string) { }, +} + +// Execute adds all child commands to the root command and sets flags appropriately. +// This is called by main.main(). It only needs to happen once to the rootCmd. +func Execute() { + err := rootCmd.Execute() + if err != nil { + os.Exit(1) + } +} + +func init() { + rootCmd.CompletionOptions.DisableDefaultCmd = true + // Here you will define your flags and configuration settings. + // Cobra supports persistent flags, which, if defined here, + // will be global for your application. + + // rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.cli.yaml)") + + // Cobra also supports local flags, which will only run + // when this action is called directly. + rootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") +} diff --git a/cmd/cli/main.go b/cmd/cli/main.go new file mode 100644 index 0000000..4d78b7d --- /dev/null +++ b/cmd/cli/main.go @@ -0,0 +1,27 @@ +/* +Copyright © 2024 NAME HERE +*/ +package main + +import ( + "log" + "os" + + "github.com/hculpan/areweplaying/cmd/cli/cmd" + "github.com/hculpan/areweplaying/pkg/utils" + "github.com/joho/godotenv" +) + +func main() { + err := godotenv.Load() + if err != nil { + log.Fatal(err) + } + + logFile := os.Getenv("CLI_LOG_FILE") + if len(logFile) > 0 { + utils.SetLogFile(logFile) + } + + cmd.Execute() +} diff --git a/cmd/cli/main.go.bak b/cmd/cli/main.go.bak new file mode 100644 index 0000000..7b7f9f7 --- /dev/null +++ b/cmd/cli/main.go.bak @@ -0,0 +1,123 @@ +package main + +import ( + "fmt" + "log" + "net/smtp" + "os" + "strconv" + + "github.com/emersion/go-imap" + "github.com/emersion/go-imap/client" + "github.com/joho/godotenv" +) + +func main() { + var username string + var password string + var fetchServer string + var sendServer string + + err := godotenv.Load() + if err != nil { + log.Fatalf("unable to load env: %s", err) + } + + username = os.Getenv("USERNAME") + password = os.Getenv("PASSWORD") + fetchServer = os.Getenv("IMAP_SERVER") + sendServer = os.Getenv("SMTP_SERVER") + port := 993 + + if len(username) == 0 || len(password) == 0 || len(fetchServer) == 0 || len(sendServer) == 0 { + log.Fatal("missing configuration variables") + } + + imapClient, err := connectToServer(username, password, fetchServer, port) + if err != nil { + log.Fatal(err) + } + defer imapClient.Logout() + + if err := fetchEmails(imapClient); err != nil { + log.Fatal(err) + } + + /* + to := "harry@culpan.org" + subject := "Test Email" + body := "This is a test email sent from a Go-based email client." + + if err := sendEmail(username, password, sendServer, 587, to, subject, body); err != nil { + log.Fatal(err) + } + */ +} + +func fetchEmails(imapClient *client.Client) error { + // Select the mailbox you want to read + _, err := imapClient.Select("INBOX", false) + if err != nil { + return err + } + + criteria := imap.NewSearchCriteria() + criteria.WithoutFlags = []string{"\\Seen"} + uids, err := imapClient.Search(criteria) + if err != nil { + log.Printf("Search error: %s\n", err) + } + + if len(uids) > 0 { + // Define the range of emails to fetch + seqSet := new(imap.SeqSet) + seqSet.AddNum(uids...) + + // Fetch the required message attributes + messages := make(chan *imap.Message, 10) + section := &imap.BodySectionName{} + items := []imap.FetchItem{section.FetchItem(), imap.FetchEnvelope} + + go func() { + if err := imapClient.Fetch(seqSet, items, messages); err != nil { + log.Fatal("Fetch error: " + err.Error()) + } + }() + + for msg := range messages { + fmt.Println("Subject:", msg.Envelope.Subject) + } + } + + return nil +} + +func sendEmail(username, password, server string, port int, to, subject, body string) error { + auth := smtp.PlainAuth("", username, password, server) + + msg := "From: " + username + "\r\n" + + "To: " + to + "\r\n" + + "Subject: " + subject + "\r\n" + + "\r\n" + + body + + err := smtp.SendMail(server+":"+strconv.Itoa(port), auth, username, []string{to}, []byte(msg)) + if err != nil { + return err + } + + return nil +} + +func connectToServer(username, password, server string, port int) (*client.Client, error) { + c, err := client.DialTLS(fmt.Sprintf("%s:%d", server, port), nil) + if err != nil { + return nil, err + } + + if err := c.Login(username, password); err != nil { + return nil, err + } + + return c, nil +} diff --git a/cmd/web/main.go b/cmd/web/main.go new file mode 100644 index 0000000..7dbe670 --- /dev/null +++ b/cmd/web/main.go @@ -0,0 +1,68 @@ +package main + +import ( + "fmt" + "log" + "net/http" + "os" + "path/filepath" + "strings" + + "github.com/go-chi/chi/v5" + "github.com/hculpan/areweplaying/pkg/utils" + "github.com/joho/godotenv" +) + +var appKey []byte + +func main() { + var port string + + err := godotenv.Load() + if err != nil { + port = "8080" + } + + port = os.Getenv("PORT") + appKey = []byte(os.Getenv("APP_KEY")) + if len(appKey) == 0 { + log.Fatal("unable to load APP_KEY") + } + + logFile := os.Getenv("WEB_LOG_FILE") + if len(logFile) > 0 { + utils.SetLogFile(logFile) + } + + r := chi.NewRouter() + + workDir, _ := os.Getwd() + filesDir := filepath.Join(workDir, "static") + FileServer(r, "/static", http.Dir(filesDir)) + + routes(r) + + log.Default().Printf("Starting up server on port :%s\n", port) + http.ListenAndServe(fmt.Sprintf(":%s", port), r) +} + +// FileServer conveniently sets up a http.FileServer handler to serve +// static files from a http.FileSystem. +func FileServer(r chi.Router, path string, root http.FileSystem) { + if strings.ContainsAny(path, "{}*") { + panic("FileServer does not permit URL parameters.") + } + + if path != "/" && path[len(path)-1] != '/' { + r.Get(path, http.RedirectHandler(path+"/", 301).ServeHTTP) + path += "/" + } + path += "*" + + r.Get(path, func(w http.ResponseWriter, r *http.Request) { + rctx := chi.RouteContext(r.Context()) + pathPrefix := strings.TrimSuffix(rctx.RoutePattern(), "/*") + fs := http.StripPrefix(pathPrefix, http.FileServer(root)) + fs.ServeHTTP(w, r) + }) +} diff --git a/cmd/web/routes.go b/cmd/web/routes.go new file mode 100644 index 0000000..01c2b1d --- /dev/null +++ b/cmd/web/routes.go @@ -0,0 +1,156 @@ +package main + +import ( + "context" + "fmt" + "log" + "net/http" + "strings" + "time" + + "github.com/go-chi/chi/v5" + "github.com/golang-jwt/jwt/v4" + "github.com/hculpan/areweplaying/cmd/web/templates" + "github.com/hculpan/areweplaying/pkg/data" +) + +func routes(r *chi.Mux) { + r.Post("/save-setup", func(w http.ResponseWriter, r *http.Request) { + saveSetupHandler(w, r) + }) + + r.Get("/", func(w http.ResponseWriter, r *http.Request) { + comp := templates.MainPage() + comp.Render(context.Background(), w) + }) + + r.Post("/setup", func(w http.ResponseWriter, r *http.Request) { + setupHandler(w, r) + }) + + r.Get("/setup", func(w http.ResponseWriter, r *http.Request) { + setupHandler(w, r) + }) + + r.Post("/send-email", func(w http.ResponseWriter, r *http.Request) { + sendEmailHandler(w, r) + }) + + r.Get("/login", func(w http.ResponseWriter, r *http.Request) { + if ValidateJWTFromCookie(r, appKey) { + http.Redirect(w, r, "/setup", http.StatusSeeOther) + } + comp := templates.Login() + comp.Render(context.Background(), w) + }) + + r.Get("/send-reminder", func(w http.ResponseWriter, r *http.Request) { + sendReminderHandler(w, r) + }) + + r.Get("/sessioninfo", func(w http.ResponseWriter, r *http.Request) { + session, dataErr := data.ReadSessionData() + if dataErr != nil { + http.Error(w, dataErr.Error(), http.StatusInternalServerError) + return + } + + playerName := r.URL.Query().Get("playerName") + playerAttending := r.URL.Query().Get("playerAttending") + + if len(playerName) > 0 && len(playerAttending) > 0 { + if err := updatePlayerAttending(playerName, playerAttending, session); err != nil { + log.Default().Println(err.Error()) + } + } + + comp := templates.PageBody(session) + comp.Render(context.Background(), w) + }) + +} + +func ValidateJWTFromCookie(r *http.Request, key []byte) bool { + // Retrieve the JWT token from the cookie + cookie, err := r.Cookie("jwt_token") + if err != nil { + // Cookie not found + return false + } + + tokenString := cookie.Value + + // Parse the token + token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) { + // Validate the algorithm used for the token + if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { + return nil, nil + } + return key, nil + }) + + if err != nil { + return false + } + + // Check if token is valid + if _, ok := token.Claims.(jwt.MapClaims); ok && token.Valid { + // Here you can also add more checks on claims if needed + // For example, check the issuer, subject, or expiration + return true + } + + return false +} + +func GenerateJWTAndStoreInCookie(w http.ResponseWriter, key []byte) error { + // Create a new token object, specifying signing method and the claims you would like it to contain. + token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{ + "exp": time.Now().Add(90 * 24 * time.Hour).Unix(), // 90 days expiration + }) + + // Sign and get the complete encoded token as a string using the secret + tokenString, err := token.SignedString(key) + if err != nil { + return err + } + + // Create a cookie + cookie := http.Cookie{ + Name: "jwt_token", + Value: tokenString, + Expires: time.Now().Add(90 * 24 * time.Hour), + HttpOnly: true, // HttpOnly if you want to prevent access to the cookie from JavaScript + } + + // Set the cookie + http.SetCookie(w, &cookie) + + return nil +} + +func updatePlayerAttending(name, attending string, session data.Session) error { + for i, p := range session.Players { + if p.Name == name { + switch strings.ToLower(attending) { + case "yes", "no": + session.Players[i].Attending = strings.ToLower(attending) + return data.PersistSession(session) + case "toggle": + newAttending := session.Players[i].Attending + switch newAttending { + case "unknown", "no": + newAttending = "yes" + case "yes": + newAttending = "no" + } + session.Players[i].Attending = newAttending + return data.PersistSession(session) + default: + return fmt.Errorf("unrecognized attending indicator %q", attending) + } + } + } + + return fmt.Errorf("player %q not found", name) +} diff --git a/cmd/web/savesetuphandler.go b/cmd/web/savesetuphandler.go new file mode 100644 index 0000000..21638eb --- /dev/null +++ b/cmd/web/savesetuphandler.go @@ -0,0 +1,67 @@ +package main + +import ( + "context" + "fmt" + "log" + "net/http" + "strconv" + "time" + + "github.com/hculpan/areweplaying/cmd/web/templates" + "github.com/hculpan/areweplaying/pkg/data" +) + +func saveSetupHandler(w http.ResponseWriter, r *http.Request) { + r.ParseForm() + nextSession := r.FormValue("nextSession") + incDays := r.FormValue("incDays") + sessionStatus := r.FormValue("sessionStatus") + log.Default().Printf("sessionStatus = %s", sessionStatus) + + session, dataErr := data.ReadSessionData() + if dataErr != nil { + http.Error(w, dataErr.Error(), http.StatusInternalServerError) + return + } + + msg := "Save successful!" + err := updateSession(session, incDays, nextSession, sessionStatus) + if err != nil { + msg = fmt.Sprintf("Error in update: %s", err.Error()) + } + + comp := templates.SaveSetup(msg) + comp.Render(context.Background(), w) +} + +func updateSession(session data.Session, incDays, nextSession, sessionStatus string) error { + if val, err := strconv.Atoi(incDays); err == nil { + session.IncrementDays = val + } else { + return err + } + + if nextSession == "nextSession" { + if d, err := time.Parse(data.DATE_FORMAT, session.Date); err == nil { + d = d.AddDate(0, 0, session.IncrementDays) + session.Date = d.Format(data.DATE_FORMAT) + } else { + return err + } + + for i, p := range session.Players { + if p.Gm { + session.Players[i].Attending = "yes" + } else { + session.Players[i].Attending = "unknown" + } + } + } + + if len(sessionStatus) > 0 { + session.Status = sessionStatus + } + + return data.PersistSession(session) +} diff --git a/cmd/web/sendemailhandler.go b/cmd/web/sendemailhandler.go new file mode 100644 index 0000000..ceec425 --- /dev/null +++ b/cmd/web/sendemailhandler.go @@ -0,0 +1,38 @@ +package main + +import ( + "context" + "net/http" + "strings" + + "github.com/hculpan/areweplaying/cmd/web/templates" + "github.com/hculpan/areweplaying/pkg/email" +) + +func sendEmailHandler(w http.ResponseWriter, r *http.Request) { + if !ValidateJWTFromCookie(r, appKey) { + http.Error(w, "Unauthorized", http.StatusUnauthorized) + return + } + + r.ParseForm() + to := r.FormValue("to") + from := "culpanvtt@gmail.com" + subject := r.FormValue("subject") + body := r.FormValue("text") + toArray := strings.Split(to, ",") + for i := range toArray { + toArray[i] = strings.Trim(toArray[i], " \t\n\r") + } + e := email.NewEmail(toArray, from, subject, body) + + // log.Default().Printf("got email: To: %s, From: %s, Subject: %q, Body: %q", to, from, subject, body) + err := email.SendEmail(e) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + comp := templates.SendEmail("Email sent!", "") + comp.Render(context.Background(), w) +} diff --git a/cmd/web/sendreminderhandler.go b/cmd/web/sendreminderhandler.go new file mode 100644 index 0000000..5b51f87 --- /dev/null +++ b/cmd/web/sendreminderhandler.go @@ -0,0 +1,49 @@ +package main + +import ( + "context" + "fmt" + "net/http" + "strings" + + "github.com/hculpan/areweplaying/cmd/web/templates" + "github.com/hculpan/areweplaying/pkg/data" +) + +func sendReminderHandler(w http.ResponseWriter, r *http.Request) { + if !ValidateJWTFromCookie(r, appKey) { + http.Error(w, "Unauthorized", http.StatusUnauthorized) + return + } + + session, err := data.ReadSessionData() + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + to := buildToList(session) + subject := fmt.Sprintf("Game Reminder for %s", session.Date) + text := fmt.Sprintf( + `This is a game reminder for %s. Please respond with a "yes" or "no" to indicate +if you plan to attend. + +If you wish to check on the status of the game, you may do so at https://awp.culpanvtt.org. + +Thank you, +Your friendly "Are We Playing?" automated system`, session.Date) + + comp := templates.SendReminder(to, subject, text) + comp.Render(context.Background(), w) +} + +func buildToList(session data.Session) string { + emails := []string{} + for _, player := range session.Players { + if len(player.Email) > 0 { + emails = append(emails, player.Email) + } + } + + return strings.Join(emails, ", ") +} diff --git a/cmd/web/setuphandler.go b/cmd/web/setuphandler.go new file mode 100644 index 0000000..7366619 --- /dev/null +++ b/cmd/web/setuphandler.go @@ -0,0 +1,31 @@ +package main + +import ( + "context" + "net/http" + + "github.com/hculpan/areweplaying/cmd/web/templates" + "github.com/hculpan/areweplaying/pkg/data" +) + +func setupHandler(w http.ResponseWriter, r *http.Request) { + session, dataErr := data.ReadSessionData() + if dataErr != nil { + http.Error(w, dataErr.Error(), http.StatusInternalServerError) + return + } + + if !ValidateJWTFromCookie(r, appKey) { + r.ParseForm() + pword := r.FormValue("password") + if len(pword) == 0 || pword != session.Password { + http.Error(w, "Unauthorized", http.StatusUnauthorized) + return + } + + GenerateJWTAndStoreInCookie(w, appKey) + } + + comp := templates.Setup(session) + comp.Render(context.Background(), w) +} diff --git a/cmd/web/templates/header.templ b/cmd/web/templates/header.templ new file mode 100644 index 0000000..a0dd768 --- /dev/null +++ b/cmd/web/templates/header.templ @@ -0,0 +1,14 @@ +package templates + +templ Header() { + + + Are We Playing? + + + + +} \ No newline at end of file diff --git a/cmd/web/templates/header_templ.go b/cmd/web/templates/header_templ.go new file mode 100644 index 0000000..c523b38 --- /dev/null +++ b/cmd/web/templates/header_templ.go @@ -0,0 +1,53 @@ +// Code generated by templ - DO NOT EDIT. + +// templ: version: v0.2.513 +package templates + +//lint:file-ignore SA4006 This context is only used if a nested component is present. + +import "github.com/a-h/templ" +import "context" +import "io" +import "bytes" + +func Header() templ.Component { + return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) { + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer) + if !templ_7745c5c3_IsBuffer { + templ_7745c5c3_Buffer = templ.GetBuffer() + defer templ.ReleaseBuffer(templ_7745c5c3_Buffer) + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var1 := templ.GetChildren(ctx) + if templ_7745c5c3_Var1 == nil { + templ_7745c5c3_Var1 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Var2 := `Are We Playing?` + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var2) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if !templ_7745c5c3_IsBuffer { + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W) + } + return templ_7745c5c3_Err + }) +} diff --git a/cmd/web/templates/index.templ b/cmd/web/templates/index.templ new file mode 100644 index 0000000..da9125a --- /dev/null +++ b/cmd/web/templates/index.templ @@ -0,0 +1,33 @@ +package templates + +templ MainPage() { + + +@Header() + + +
+
+ +
+ +
+ + + + + +} \ No newline at end of file diff --git a/cmd/web/templates/index_templ.go b/cmd/web/templates/index_templ.go new file mode 100644 index 0000000..0cd7655 --- /dev/null +++ b/cmd/web/templates/index_templ.go @@ -0,0 +1,53 @@ +// Code generated by templ - DO NOT EDIT. + +// templ: version: v0.2.513 +package templates + +//lint:file-ignore SA4006 This context is only used if a nested component is present. + +import "github.com/a-h/templ" +import "context" +import "io" +import "bytes" + +func MainPage() templ.Component { + return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) { + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer) + if !templ_7745c5c3_IsBuffer { + templ_7745c5c3_Buffer = templ.GetBuffer() + defer templ.ReleaseBuffer(templ_7745c5c3_Buffer) + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var1 := templ.GetChildren(ctx) + if templ_7745c5c3_Var1 == nil { + templ_7745c5c3_Var1 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = Header().Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if !templ_7745c5c3_IsBuffer { + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W) + } + return templ_7745c5c3_Err + }) +} diff --git a/cmd/web/templates/login.templ b/cmd/web/templates/login.templ new file mode 100644 index 0000000..1d7e289 --- /dev/null +++ b/cmd/web/templates/login.templ @@ -0,0 +1,31 @@ +package templates + +templ Login() { + + + +@Header() + + +
+
+
+
+

Please sign in

+ +
+ + +
+ + +
+
+
+
+ + + + +} \ No newline at end of file diff --git a/cmd/web/templates/login_templ.go b/cmd/web/templates/login_templ.go new file mode 100644 index 0000000..619ac0d --- /dev/null +++ b/cmd/web/templates/login_templ.go @@ -0,0 +1,70 @@ +// Code generated by templ - DO NOT EDIT. + +// templ: version: v0.2.513 +package templates + +//lint:file-ignore SA4006 This context is only used if a nested component is present. + +import "github.com/a-h/templ" +import "context" +import "io" +import "bytes" + +func Login() templ.Component { + return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) { + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer) + if !templ_7745c5c3_IsBuffer { + templ_7745c5c3_Buffer = templ.GetBuffer() + defer templ.ReleaseBuffer(templ_7745c5c3_Buffer) + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var1 := templ.GetChildren(ctx) + if templ_7745c5c3_Var1 == nil { + templ_7745c5c3_Var1 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = Header().Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Var2 := `Please sign in` + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var2) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if !templ_7745c5c3_IsBuffer { + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W) + } + return templ_7745c5c3_Err + }) +} diff --git a/cmd/web/templates/pagebody.templ b/cmd/web/templates/pagebody.templ new file mode 100644 index 0000000..e94207d --- /dev/null +++ b/cmd/web/templates/pagebody.templ @@ -0,0 +1,50 @@ +package templates + +import ( +"github.com/hculpan/areweplaying/pkg/data" +) + +templ PageBody(session data.Session) { +
+
Next Session: { session.Date }
+
+
+
+ + + + + + + + + for _, player := range session.Players { + + + + + } + +
AttendingPlayer
+ if player.Attending == "yes" { + + } else if player.Attending == "no"{ + + } else { + + } + { player.Name }
+
+
+
+ if session.Status == "planned" { +
Status: { session.Status }
+ } else if session.Status == "canceled" { +
Status: { session.Status }
+ } else { +
Status: { session.Status }
+ } +
+ +} \ No newline at end of file diff --git a/cmd/web/templates/pagebody_templ.go b/cmd/web/templates/pagebody_templ.go new file mode 100644 index 0000000..5bd05b1 --- /dev/null +++ b/cmd/web/templates/pagebody_templ.go @@ -0,0 +1,200 @@ +// Code generated by templ - DO NOT EDIT. + +// templ: version: v0.2.513 +package templates + +//lint:file-ignore SA4006 This context is only used if a nested component is present. + +import "github.com/a-h/templ" +import "context" +import "io" +import "bytes" + +import ( + "github.com/hculpan/areweplaying/pkg/data" +) + +func PageBody(session data.Session) templ.Component { + return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) { + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer) + if !templ_7745c5c3_IsBuffer { + templ_7745c5c3_Buffer = templ.GetBuffer() + defer templ.ReleaseBuffer(templ_7745c5c3_Buffer) + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var1 := templ.GetChildren(ctx) + if templ_7745c5c3_Var1 == nil { + templ_7745c5c3_Var1 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Var2 := `Next Session: ` + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var2) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var3 string + templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(session.Date) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/web/templates/pagebody.templ`, Line: 8, Col: 64} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + for _, player := range session.Players { + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Var4 := `Attending` + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var4) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Var5 := `Player` + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var5) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if player.Attending == "yes" { + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } else if player.Attending == "no" { + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } else { + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var6 string + templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(player.Name) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/web/templates/pagebody.templ`, Line: 32, Col: 105} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if session.Status == "planned" { + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Var7 := `Status: ` + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var7) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var8 string + templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(session.Status) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/web/templates/pagebody.templ`, Line: 41, Col: 73} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } else if session.Status == "canceled" { + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Var9 := `Status: ` + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var9) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var10 string + templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(session.Status) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/web/templates/pagebody.templ`, Line: 43, Col: 72} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } else { + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Var11 := `Status: ` + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var11) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var12 string + templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(session.Status) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/web/templates/pagebody.templ`, Line: 45, Col: 60} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if !templ_7745c5c3_IsBuffer { + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W) + } + return templ_7745c5c3_Err + }) +} diff --git a/cmd/web/templates/save_setup.templ b/cmd/web/templates/save_setup.templ new file mode 100644 index 0000000..562b0e2 --- /dev/null +++ b/cmd/web/templates/save_setup.templ @@ -0,0 +1,28 @@ +package templates + +templ SaveSetup(status string) { + +@Header() + + +
+ +
+

{ status }

+
+
+ +
+
+ + + +} \ No newline at end of file diff --git a/cmd/web/templates/save_setup_templ.go b/cmd/web/templates/save_setup_templ.go new file mode 100644 index 0000000..e734f92 --- /dev/null +++ b/cmd/web/templates/save_setup_templ.go @@ -0,0 +1,65 @@ +// Code generated by templ - DO NOT EDIT. + +// templ: version: v0.2.513 +package templates + +//lint:file-ignore SA4006 This context is only used if a nested component is present. + +import "github.com/a-h/templ" +import "context" +import "io" +import "bytes" + +func SaveSetup(status string) templ.Component { + return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) { + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer) + if !templ_7745c5c3_IsBuffer { + templ_7745c5c3_Buffer = templ.GetBuffer() + defer templ.ReleaseBuffer(templ_7745c5c3_Buffer) + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var1 := templ.GetChildren(ctx) + if templ_7745c5c3_Var1 == nil { + templ_7745c5c3_Var1 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = Header().Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var3 string + templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(status) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/web/templates/save_setup.templ`, Line: 18, Col: 24} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if !templ_7745c5c3_IsBuffer { + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W) + } + return templ_7745c5c3_Err + }) +} diff --git a/cmd/web/templates/sendemail.templ b/cmd/web/templates/sendemail.templ new file mode 100644 index 0000000..ba24ea9 --- /dev/null +++ b/cmd/web/templates/sendemail.templ @@ -0,0 +1,31 @@ +package templates + +templ SendEmail(status string, msg string) { + +@Header() + + +
+ +
+

{ status }

+
+
+

{ msg }

+
+
+ +
+
+ + + +} \ No newline at end of file diff --git a/cmd/web/templates/sendemail_templ.go b/cmd/web/templates/sendemail_templ.go new file mode 100644 index 0000000..14e4f6e --- /dev/null +++ b/cmd/web/templates/sendemail_templ.go @@ -0,0 +1,78 @@ +// Code generated by templ - DO NOT EDIT. + +// templ: version: v0.2.513 +package templates + +//lint:file-ignore SA4006 This context is only used if a nested component is present. + +import "github.com/a-h/templ" +import "context" +import "io" +import "bytes" + +func SendEmail(status string, msg string) templ.Component { + return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) { + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer) + if !templ_7745c5c3_IsBuffer { + templ_7745c5c3_Buffer = templ.GetBuffer() + defer templ.ReleaseBuffer(templ_7745c5c3_Buffer) + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var1 := templ.GetChildren(ctx) + if templ_7745c5c3_Var1 == nil { + templ_7745c5c3_Var1 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = Header().Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var3 string + templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(status) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/web/templates/sendemail.templ`, Line: 18, Col: 24} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var4 string + templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(msg) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/web/templates/sendemail.templ`, Line: 21, Col: 20} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if !templ_7745c5c3_IsBuffer { + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W) + } + return templ_7745c5c3_Err + }) +} diff --git a/cmd/web/templates/sendreminder.templ b/cmd/web/templates/sendreminder.templ new file mode 100644 index 0000000..ea383fc --- /dev/null +++ b/cmd/web/templates/sendreminder.templ @@ -0,0 +1,47 @@ +package templates + + +templ SendReminder(to string, subject string, text string) { + +@Header() + + +
+ +
+

Send Reminder

+
+
+
+
+ + +
+
+ + +
+
+ + +
+ +
+
+
+ +
+
+ + + + +} \ No newline at end of file diff --git a/cmd/web/templates/sendreminder_templ.go b/cmd/web/templates/sendreminder_templ.go new file mode 100644 index 0000000..0a77e7e --- /dev/null +++ b/cmd/web/templates/sendreminder_templ.go @@ -0,0 +1,126 @@ +// Code generated by templ - DO NOT EDIT. + +// templ: version: v0.2.513 +package templates + +//lint:file-ignore SA4006 This context is only used if a nested component is present. + +import "github.com/a-h/templ" +import "context" +import "io" +import "bytes" + +func SendReminder(to string, subject string, text string) templ.Component { + return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) { + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer) + if !templ_7745c5c3_IsBuffer { + templ_7745c5c3_Buffer = templ.GetBuffer() + defer templ.ReleaseBuffer(templ_7745c5c3_Buffer) + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var1 := templ.GetChildren(ctx) + if templ_7745c5c3_Var1 == nil { + templ_7745c5c3_Var1 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = Header().Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Var3 := `Send Reminder` + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var3) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if !templ_7745c5c3_IsBuffer { + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W) + } + return templ_7745c5c3_Err + }) +} diff --git a/cmd/web/templates/setup.templ b/cmd/web/templates/setup.templ new file mode 100644 index 0000000..edc5dba --- /dev/null +++ b/cmd/web/templates/setup.templ @@ -0,0 +1,73 @@ +package templates + +import ( +"strconv" +"github.com/hculpan/areweplaying/pkg/data" +) + +templ Setup(session data.Session) { + +@Header() + + +
+ +
+

Setup

+
+
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ +
+
+
+ +
+
+ + + +} \ No newline at end of file diff --git a/cmd/web/templates/setup_templ.go b/cmd/web/templates/setup_templ.go new file mode 100644 index 0000000..00fadd5 --- /dev/null +++ b/cmd/web/templates/setup_templ.go @@ -0,0 +1,146 @@ +// Code generated by templ - DO NOT EDIT. + +// templ: version: v0.2.513 +package templates + +//lint:file-ignore SA4006 This context is only used if a nested component is present. + +import "github.com/a-h/templ" +import "context" +import "io" +import "bytes" + +import ( + "github.com/hculpan/areweplaying/pkg/data" + "strconv" +) + +func Setup(session data.Session) templ.Component { + return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) { + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer) + if !templ_7745c5c3_IsBuffer { + templ_7745c5c3_Buffer = templ.GetBuffer() + defer templ.ReleaseBuffer(templ_7745c5c3_Buffer) + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var1 := templ.GetChildren(ctx) + if templ_7745c5c3_Var1 == nil { + templ_7745c5c3_Var1 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = Header().Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Var4 := `Setup` + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var4) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if !templ_7745c5c3_IsBuffer { + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W) + } + return templ_7745c5c3_Err + }) +} diff --git a/exmaple_players.json b/exmaple_players.json new file mode 100644 index 0000000..84b9d1d --- /dev/null +++ b/exmaple_players.json @@ -0,0 +1,16 @@ +{ + "date": "Tuesday Jan 23, 2024 6:30pm", + "players": [ + { + "name": "Player1", + "attending": "unknown" + }, + { + "name": "Player2", + "attending": "yes" + } + ], + "inc_days": 7, + "status": "planned", + "password": "ABC123" +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..2a11ea0 --- /dev/null +++ b/go.mod @@ -0,0 +1,22 @@ +module github.com/hculpan/areweplaying + +go 1.21.6 + +require ( + github.com/a-h/templ v0.2.513 + github.com/go-chi/chi/v5 v5.0.11 +) + +require ( + github.com/emersion/go-imap v1.2.1 + github.com/golang-jwt/jwt/v4 v4.5.0 + github.com/joho/godotenv v1.5.1 + github.com/spf13/cobra v1.8.0 +) + +require ( + github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/spf13/pflag v1.0.5 // indirect + golang.org/x/text v0.3.7 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..559845d --- /dev/null +++ b/go.sum @@ -0,0 +1,30 @@ +github.com/a-h/templ v0.2.513 h1:ZmwGAOx4NYllnHy+FTpusc4+c5msoMpPIYX0Oy3dNqw= +github.com/a-h/templ v0.2.513/go.mod h1:9gZxTLtRzM3gQxO8jr09Na0v8/jfliS97S9W5SScanM= +github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/emersion/go-imap v1.2.1 h1:+s9ZjMEjOB8NzZMVTM3cCenz2JrQIGGo5j1df19WjTA= +github.com/emersion/go-imap v1.2.1/go.mod h1:Qlx1FSx2FTxjnjWpIlVNEuX+ylerZQNFE5NsmKFSejY= +github.com/emersion/go-message v0.15.0/go.mod h1:wQUEfE+38+7EW8p8aZ96ptg6bAb1iwdgej19uXASlE4= +github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21 h1:OJyUGMJTzHTd1XQp98QTaHernxMYzRaOasRir9hUlFQ= +github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ= +github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594/go.mod h1:aqO8z8wPrjkscevZJFVE1wXJrLpC5LtJG7fqLOsPb2U= +github.com/go-chi/chi/v5 v5.0.11 h1:BnpYbFZ3T3S1WMpD79r7R5ThWX40TaFB7L31Y8xqSwA= +github.com/go-chi/chi/v5 v5.0.11/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= +github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg= +github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0= +github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/pkg/data/data.go b/pkg/data/data.go new file mode 100644 index 0000000..03e1cc8 --- /dev/null +++ b/pkg/data/data.go @@ -0,0 +1,31 @@ +package data + +const ( + DATE_FORMAT string = "Monday Jan 2, 2006 3:04pm" +) + +type Player struct { + Name string `json:"name"` + Attending string `json:"attending"` + ToggleUrl string `json:"-"` + Email string `json:"email"` + Gm bool `json:"gm"` +} + +type Session struct { + Date string `json:"date"` + Players []Player `json:"players"` + IncrementDays int `json:"inc_days"` + Status string `json:"status"` + Password string `json:"password"` +} + +func (s *Session) GetPlayerByEmail(addr string) *Player { + for i, p := range s.Players { + if addr == p.Email { + return &s.Players[i] + } + } + + return nil +} diff --git a/pkg/data/loaddata.go b/pkg/data/loaddata.go new file mode 100644 index 0000000..1297f2c --- /dev/null +++ b/pkg/data/loaddata.go @@ -0,0 +1,35 @@ +package data + +import ( + "encoding/json" + "os" +) + +func ReadSessionData() (Session, error) { + var session Session + data, err := os.ReadFile("players.json") + if err != nil { + return session, err + } + err = json.Unmarshal(data, &session) + + if err == nil { + for i, p := range session.Players { + session.Players[i].ToggleUrl = "/sessioninfo?playerName=" + p.Name + "&playerAttending=toggle" + } + } + + return session, err +} + +func PersistSession(session Session) error { + // Convert the object to JSON + jsonData, err := json.MarshalIndent(session, "", " ") + if err != nil { + return err + } + + // Write JSON data to file + err = os.WriteFile("players.json", jsonData, 0644) + return err +} diff --git a/pkg/email/config.go b/pkg/email/config.go new file mode 100644 index 0000000..28ab912 --- /dev/null +++ b/pkg/email/config.go @@ -0,0 +1,75 @@ +package email + +import ( + "fmt" + "os" + + "github.com/emersion/go-imap/client" + "github.com/joho/godotenv" +) + +const ( + IMAP_SERVICE EmailService = 993 + SMTP_SERVICE EmailService = 587 +) + +type EmailService int + +type Config struct { + Username string + Password string + ImapServer string + SmtpServer string +} + +func NewConfig(username, password, imapServer, smtpServer string) *Config { + return &Config{ + Username: username, + Password: password, + ImapServer: imapServer, + SmtpServer: smtpServer, + } +} + +func NewConfigFromEnv() (*Config, error) { + var username string + var password string + var fetchServer string + var sendServer string + + err := godotenv.Load() + if err != nil { + return nil, fmt.Errorf("error loading .env file: %s", err) + } + + username = os.Getenv("USERNAME") + password = os.Getenv("PASSWORD") + fetchServer = os.Getenv("IMAP_SERVER") + sendServer = os.Getenv("SMTP_SERVER") + + if len(username) == 0 || len(password) == 0 || len(fetchServer) == 0 || len(sendServer) == 0 { + return nil, fmt.Errorf("missing configuration variables") + } + + return NewConfig(username, password, fetchServer, sendServer), nil +} + +func (c *Config) ConnectToServer(service EmailService) (*client.Client, error) { + port := int(service) + var host string + if service == SMTP_SERVICE { + host = c.SmtpServer + } else { + host = c.ImapServer + } + client, err := client.DialTLS(fmt.Sprintf("%s:%d", host, port), nil) + if err != nil { + return nil, err + } + + if err := client.Login(c.Username, c.Password); err != nil { + return nil, err + } + + return client, nil +} diff --git a/pkg/email/email.go b/pkg/email/email.go new file mode 100644 index 0000000..5b9b98b --- /dev/null +++ b/pkg/email/email.go @@ -0,0 +1,27 @@ +package email + +import "fmt" + +type Email struct { + ToAddress []string + FromAddress string + Subject string + Body string +} + +func NewEmail(to []string, from, subject, body string) *Email { + return &Email{ + ToAddress: to, + FromAddress: from, + Subject: subject, + Body: body, + } +} + +func (e *Email) String() string { + bodyLength := len(e.Body) + if bodyLength > 10 { + bodyLength = 10 + } + return fmt.Sprintf("To: %s, From: %s, Subject: %s, Body: %s", e.ToAddress, e.FromAddress, e.Subject, e.Body[:bodyLength]) +} diff --git a/pkg/email/fetch.go b/pkg/email/fetch.go new file mode 100644 index 0000000..961bf61 --- /dev/null +++ b/pkg/email/fetch.go @@ -0,0 +1,100 @@ +package email + +import ( + "fmt" + "io" + "log" + "net/mail" + "strings" + + "github.com/emersion/go-imap" + "github.com/emersion/go-imap/client" +) + +func FetchEmails() ([]Email, error) { + config, err := NewConfigFromEnv() + if err != nil { + return nil, err + } + + imapClient, err := config.ConnectToServer(IMAP_SERVICE) + if err != nil { + log.Fatal(err) + } + defer imapClient.Logout() + + return fetchEmails(imapClient) +} + +func concatAddresses(addresses []*imap.Address) string { + result := []string{} + + for _, address := range addresses { + if len(address.PersonalName) > 0 { + result = append(result, fmt.Sprintf("%s <%s>", address.PersonalName, address.Address())) + } else { + result = append(result, address.Address()) + } + + } + + return strings.Join(result, "; ") +} + +func fetchEmails(imapClient *client.Client) ([]Email, error) { + result := []Email{} + + // Select the mailbox you want to read + _, err := imapClient.Select("INBOX", false) + if err != nil { + return nil, err + } + + criteria := imap.NewSearchCriteria() + criteria.WithoutFlags = []string{imap.SeenFlag} + uids, err := imapClient.Search(criteria) + if err != nil { + log.Printf("Search error: %s\n", err) + } + + if len(uids) > 0 { + // Define the range of emails to fetch + seqSet := new(imap.SeqSet) + seqSet.AddNum(uids...) + + // Fetch the required message attributes + messages := make(chan *imap.Message, 10) + section := &imap.BodySectionName{} + items := []imap.FetchItem{section.FetchItem(), imap.FetchEnvelope} + + go func() { + if err := imapClient.Fetch(seqSet, items, messages); err != nil { + log.Fatal("Fetch error: " + err.Error()) + } + }() + + for msg := range messages { + toAddress := concatAddresses(msg.Envelope.To) + fromAddress := msg.Envelope.From[0].Address() + r := msg.GetBody(section) + if r == nil { + return result, fmt.Errorf("server didn't returned message body") + } + m, err := mail.ReadMessage(r) + if err != nil { + return result, err + } + body, err := io.ReadAll(m.Body) + + email := NewEmail( + []string{toAddress}, + fromAddress, + msg.Envelope.Subject, + string(body), + ) + result = append(result, *email) + } + } + + return result, nil +} diff --git a/pkg/email/send.go b/pkg/email/send.go new file mode 100644 index 0000000..31e60a1 --- /dev/null +++ b/pkg/email/send.go @@ -0,0 +1,37 @@ +package email + +import ( + "net/smtp" + "strconv" + "strings" +) + +func SendEmail(email *Email) error { + config, err := NewConfigFromEnv() + if err != nil { + return err + } + + if err == nil { + err = sendEmail(config.Username, config.Password, config.SmtpServer, int(SMTP_SERVICE), email.ToAddress, email.Subject, email.Body) + } + + return err +} + +func sendEmail(username, password, server string, port int, to []string, subject, body string) error { + auth := smtp.PlainAuth("", username, password, server) + + msg := "From: " + username + "\r\n" + + "To: " + strings.Join(to, ",") + "\r\n" + + "Subject: " + subject + "\r\n" + + "\r\n" + + body + + err := smtp.SendMail(server+":"+strconv.Itoa(port), auth, username, to, []byte(msg)) + if err != nil { + return err + } + + return nil +} diff --git a/pkg/utils/utils.go b/pkg/utils/utils.go new file mode 100644 index 0000000..564c567 --- /dev/null +++ b/pkg/utils/utils.go @@ -0,0 +1,18 @@ +package utils + +import ( + "log" + "os" +) + +func SetLogFile(path string) error { + logFile, err := os.OpenFile(path, os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0644) + if err != nil { + return err + } + + log.SetOutput(logFile) + log.SetFlags(log.LstdFlags | log.Lshortfile | log.Lmicroseconds) + + return nil +} diff --git a/static/check_green.png b/static/check_green.png new file mode 100644 index 0000000..7990234 Binary files /dev/null and b/static/check_green.png differ diff --git a/static/check_grey.png b/static/check_grey.png new file mode 100644 index 0000000..3928d23 Binary files /dev/null and b/static/check_grey.png differ diff --git a/static/check_red.png b/static/check_red.png new file mode 100644 index 0000000..f563511 Binary files /dev/null and b/static/check_red.png differ