package main import ( "database/sql" "encoding/json" "html/template" "log" "net/http" "strconv" _ "github.com/mattn/go-sqlite3" ) const ( PORT = "8080" DB_FILE = "./datefinder.sqlite3" ) var ( db *sql.DB indexHtmlTemplate *template.Template ) type ChoicesPost struct { PollId int `json:"pollId"` Username string `json:"username"` Choices [][]bool `json:"choices"` } func initDatabase() { var err error db, err = sql.Open("sqlite3", DB_FILE) if err != nil { log.Fatalf("[!] Could not open database file %s, error: %s\n", DB_FILE, err) } const createPollsQuery = ` CREATE TABLE IF NOT EXISTS polls ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL ); ` _, err = db.Exec(createPollsQuery) if err != nil { log.Fatalf("[!] Could not create database table polls, error: %s\n", err) } const createChoicesQuery = ` CREATE TABLE IF NOT EXISTS choices ( id INTEGER PRIMARY KEY AUTOINCREMENT, pollid INTEGER NOT NULL, username TEXT NOT NULL, choices TEXT NOT NULL, FOREIGN KEY(pollid) REFERENCES polls(id) ); ` _, err = db.Exec(createChoicesQuery) if err != nil { log.Fatalf("[!] Could not create database table choices, error: %s\n", err) } } func prepareStaticContent() { var err error indexHtmlTemplate, err = template.ParseFiles("./static/index.html.tmpl") if err != nil { log.Fatalf("[!] Could not parse index.html template, error: %s\n", err) } } func handleGetRoot(w http.ResponseWriter, r *http.Request) { var pollName string pollId, err := strconv.Atoi(r.PathValue("pollId")) if err != nil { log.Printf("[!] Invallid poll id url segment, error: %s\n", err) http.Error(w, "Poll not found", http.StatusBadRequest) return } const query = ` SELECT name FROM polls WHERE id = ?; ` err = db.QueryRow(query, pollId).Scan(&pollName) if err != nil { log.Printf("[!] No poll with id %d was found, error: %s\n", pollId, err) http.Error(w, "Poll not found", http.StatusBadRequest) return } err = indexHtmlTemplate.Execute(w, struct { PollId int PollName string }{ PollId: pollId, PollName: pollName, }) if err != nil { log.Printf("[!] Could not execute index.html template, error: %s\n", err) http.Error(w, "Error rendering template", http.StatusInternalServerError) } } func handleGetResult(w http.ResponseWriter, r *http.Request) { pollId, err := strconv.Atoi(r.PathValue("pollId")) if err != nil { log.Printf("[!] Invallid poll id url segment, error: %s\n", err) http.Error(w, "Poll not found", http.StatusBadRequest) return } const query = ` SELECT polls.name, choices.username, choices.choices FROM choices JOIN polls ON choices.pollid = polls.id WHERE choices.pollid = ? ` rows, err := db.Query(query, pollId) defer rows.Close() if err != nil { log.Printf("[!] No poll with id %d was found, error: %s\n", pollId, err) http.Error(w, "Poll not found", http.StatusBadRequest) return } var results []struct { pollName string username string choices string } for rows.Next() { var pollName string var username string var choices string if err := rows.Scan(&pollName, &username, &choices); err != nil { log.Printf("[!] Could not scan result row %v, error: %s\n", rows, err) } results = append(results, struct { pollName string username string choices string }{ pollName: pollName, username: username, choices: choices, }) } type HourlyVote struct { Usernames []string `json:"usernames"` Votes int `json:"votes"` } type PollResult struct { Id int `json:"id"` Name string `json:"name"` Votes [7][24]HourlyVote `json:"votes"` } var pollResult PollResult pollResult.Id = pollId pollResult.Name = results[0].pollName for i := 0; i < 7; i++ { for j := 0; j < 24; j++ { pollResult.Votes[i][j] = HourlyVote{ Usernames: []string{}, Votes: 0, } } } for _, result := range results { var choices [][]bool err := json.Unmarshal([]byte(result.choices), &choices) if err != nil { log.Printf("[!] Could not unmarshal choices %s, error: %s\n", result.choices, err) continue } for didx, day := range choices { for hidx, hour := range day { if hour { pollResult.Votes[didx][hidx].Usernames = append(pollResult.Votes[didx][hidx].Usernames, result.username) pollResult.Votes[didx][hidx].Votes++ } } } } w.Header().Set("Content-Type", "application/json") if err := json.NewEncoder(w).Encode(pollResult); err != nil { http.Error(w, "Failed to encode JSON", http.StatusInternalServerError) } } func handlePostSubmit(w http.ResponseWriter, r *http.Request) { log.Printf("[*] Received request %v %v\n", r.Method, r.URL) log.Printf("[*] %v\n", r.Body) w.Header().Set("Access-Control-Allow-Origin", "*") w.Header().Set("Access-Control-Allow-Methods", "OPTIONS, GET, POST") w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Access-Control-Allow-Origin") var choicesPost ChoicesPost err := json.NewDecoder(r.Body).Decode(&choicesPost) if err != nil { log.Printf("[!] Error decoding request Body %v, error: %s\n", r.Body, err) w.WriteHeader(http.StatusBadRequest) return } choicesJson, err := json.Marshal(choicesPost.Choices) if err != nil { log.Printf("[!] Could not marshal choices, error: %s\n", err) } tx, err := db.Begin() if err != nil { log.Printf("[!] Could not begin database transaction, error: %s\n", err) w.WriteHeader(http.StatusBadRequest) return } const query = ` INSERT INTO choices (pollid, username, choices) VALUES (?, ?, ?); ` _, err = tx.Exec(query, choicesPost.PollId, choicesPost.Username, string(choicesJson)) if err != nil { log.Printf("[!] Could not insert choices into database, error: %s\n", err) tx.Rollback() w.WriteHeader(http.StatusBadRequest) return } tx.Commit() log.Printf("[*] Persisted choices for poll %d, username %s to database\n", choicesPost.PollId, choicesPost.Username) w.WriteHeader(http.StatusOK) } func handleOptionsSubmit(w http.ResponseWriter, r *http.Request) { w.Header().Set("Access-Control-Allow-Origin", "*") w.Header().Set("Access-Control-Allow-Methods", "OPTIONS, GET, POST") w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Access-Control-Allow-Origin") w.WriteHeader(http.StatusNoContent) } func main() { initDatabase() defer db.Close() prepareStaticContent() http.HandleFunc( "GET /favicon.ico", func(w http.ResponseWriter, r *http.Request) { http.Error(w, "There is no favicon.ico!", http.StatusBadRequest) }, ) http.HandleFunc( "GET /{pollId}", handleGetRoot, ) http.HandleFunc( "GET /{pollId}/result", handleGetResult, ) http.HandleFunc( "POST /api/submit", handlePostSubmit, ) http.HandleFunc( "OPTIONS /api/submit", handleOptionsSubmit, ) log.Printf("[*] Datefinder server listening on :%s\n", PORT) if err := http.ListenAndServe(":8080", nil); err != nil { log.Fatalf("[!] Could not start server on %s, error: %s\n", PORT, err) } }