diff --git a/assets/logo.png b/assets/logo.png new file mode 100644 index 0000000..199ac75 Binary files /dev/null and b/assets/logo.png differ diff --git a/index.html b/index.html new file mode 100644 index 0000000..e1fbb62 --- /dev/null +++ b/index.html @@ -0,0 +1,5 @@ + + +

Welcome to Chirpy

+ + diff --git a/main.go b/main.go index a3dd973..2602047 100644 --- a/main.go +++ b/main.go @@ -1,7 +1,178 @@ package main -import "fmt" +import ( + "context" + "encoding/json" + "errors" + "fmt" + "log" + "net/http" + "os" + "os/signal" + "slices" + "strings" + "sync/atomic" + "syscall" + "time" +) + +type server struct { + fileServerHits atomic.Int32 +} + +type chirpRequest struct { + Body string `json:"body"` +} + +type validationError struct { + Err string `json:"error"` +} + +type chirpResponse struct { + CleanedBody string `json:"cleaned_body"` +} + +func writeJsonResponse(w http.ResponseWriter, response any) { + encoder := json.NewEncoder(w) + + if err := encoder.Encode(response); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } +} + +func (s *server) HandleFiles(prefix string) http.Handler { + fileServer := http.StripPrefix(prefix, http.FileServer(http.Dir("."))) + + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + s.fileServerHits.Add(1) + fileServer.ServeHTTP(w, r) + }) +} + +func (s *server) HandleHealthChecks(w http.ResponseWriter, r *http.Request) { + w.Header().Add("Content-Type", "text/plain; charset=utf-8") + w.WriteHeader(http.StatusOK) + w.Write([]byte("OK")) +} + +func (s *server) HandleReset(w http.ResponseWriter, r *http.Request) { + s.fileServerHits.Store(0) + + w.Header().Add("Content-Type", "text/plain; charset=utf-8") + w.WriteHeader(http.StatusOK) + w.Write([]byte("OK")) +} + +func (s *server) HandleMetrics(w http.ResponseWriter, r *http.Request) { + hits := s.fileServerHits.Load() + + w.Header().Add("Content-Type", "text/html; charset=utf-8") + w.WriteHeader(http.StatusOK) + + template := ` + + +

Welcome, Chirpy Admin

+

Chirpy has been visited %d times!

+ + + ` + + fmt.Fprintf(w, template, hits) +} + +func (s *server) HandleChirpValidation(w http.ResponseWriter, r *http.Request) { + blacklist := []string{ + "kerfuffle", + "sharbert", + "fornax", + } + + var chirpRequest chirpRequest + + defer r.Body.Close() + + w.Header().Add("Content-Type", "application/json") + + err := json.NewDecoder(r.Body).Decode(&chirpRequest) + + if err != nil { + w.WriteHeader(http.StatusBadRequest) + writeJsonResponse(w, validationError{ + Err: "Unable to deserialize JSON", + }) + return + } + + if len(chirpRequest.Body) > 140 { + w.WriteHeader(http.StatusBadRequest) + writeJsonResponse(w, validationError{ + Err: "Chirp is too long", + }) + return + } + + words := strings.Split(chirpRequest.Body, " ") + sanitized := []string{} + + for _, word := range words { + if slices.Contains(blacklist, strings.ToLower(word)) { + sanitized = append(sanitized, "****") + continue + } + + sanitized = append(sanitized, word) + } + + cleanedBody := strings.Join(sanitized, " ") + + w.WriteHeader(http.StatusOK) + writeJsonResponse(w, chirpResponse{CleanedBody: cleanedBody}) +} func main() { - fmt.Println("Hello, World!") + server := &server{} + mux := http.NewServeMux() + + appRoot := "/app/" + + mux.Handle(appRoot, server.HandleFiles(appRoot)) + mux.HandleFunc("GET /admin/metrics", server.HandleMetrics) + mux.HandleFunc("POST /admin/reset", server.HandleReset) + + mux.HandleFunc("POST /api/validate_chirp", server.HandleChirpValidation) + mux.HandleFunc("GET /api/healthz", server.HandleHealthChecks) + + port := ":8080" + + httpServer := http.Server{ + Handler: mux, + Addr: port, + } + + go func() { + log.Printf("Server started and listening on %s\n", port) + + err := httpServer.ListenAndServe() + + if !errors.Is(err, http.ErrServerClosed) { + log.Fatalf("HTTP Server error: %v", err) + } + + log.Println("Stopped serving new connections") + }() + + sigChan := make(chan os.Signal, 1) + signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) + <-sigChan + + shutdownCtx, shutdownRelease := context.WithTimeout(context.Background(), 10*time.Second) + + defer shutdownRelease() + + if err := httpServer.Shutdown(shutdownCtx); err != nil { + log.Fatalf("HTTP shutdown error: %v", err) + } + + log.Println("Graceful shutdown complete") } diff --git a/run.sh b/run.sh new file mode 100755 index 0000000..e59f582 --- /dev/null +++ b/run.sh @@ -0,0 +1,2 @@ +go build -o ./bin +./bin/chirpy