diff --git a/main.go b/main.go index a0cb3f5..0cd1f07 100644 --- a/main.go +++ b/main.go @@ -81,6 +81,8 @@ func main() { } defer db.Close() + ln.Log(ctx, ln.Info("starting up..."), ln.F{"db": *dbLoc, "hostname": hostname, "ts-log-loc": *tsLogLoc}) + done, tsLgr, err := getLogFout(*tsLogLoc) if err != nil { ln.FatalErr(ctx, err) diff --git a/schema/schema.sql b/schema/schema.sql index a98416d..89c1b02 100644 --- a/schema/schema.sql +++ b/schema/schema.sql @@ -37,3 +37,9 @@ CREATE TABLE IF NOT EXISTS twitch_revenue -- generated columns , total_revenue REAL GENERATED ALWAYS AS (sub_revenue + prime_revenue + gifted_subs_revenue + multi_month_gifted_subs_revenue + bits_revenue + ad_revenue + game_sales_revenue + extensions_revenue + bounties_revenue) ); + +CREATE TABLE IF NOT EXISTS sql_queries + ( name TEXT PRIMARY KEY + , query TEXT UNIQUE NOT NULL + , who TEXT NOT NULL + ); diff --git a/server.go b/server.go index 0497ffa..c16bdcb 100644 --- a/server.go +++ b/server.go @@ -4,16 +4,34 @@ import ( "context" "crypto/tls" "database/sql" + "embed" "encoding/json" "net/http" "time" "tailscale.com/client/tailscale" + "tailscale.com/client/tailscale/apitype" "tailscale.com/tsnet" "within.website/ln" "within.website/ln/ex" ) +//go:embed static/* +var staticFS embed.FS + +func withWhois(next func(whois *apitype.WhoIsResponse, w http.ResponseWriter, r *http.Request)) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + userInfo, err := tailscale.WhoIs(r.Context(), r.RemoteAddr) + if err != nil { + http.Error(w, "can't get whois response: "+err.Error(), http.StatusInternalServerError) + ln.Error(r.Context(), err) + return + } + + next(userInfo, w, r) + }) +} + type Server struct { db *sql.DB mux *http.ServeMux @@ -30,6 +48,9 @@ func NewServer(db *sql.DB, srv *tsnet.Server) *Server { } mux.HandleFunc("/api/whois", s.whois) + mux.HandleFunc("/api/importcsv", s.importCSV) + mux.HandleFunc("/api/queries/get", s.getQueries) + mux.Handle("/static/", http.FileServer(http.FS(staticFS))) return s } @@ -60,7 +81,7 @@ func (s *Server) ListenAndServe() error { return hs.Serve(l) } -func (s Server) whois(w http.ResponseWriter, r *http.Request) { +func (s *Server) whois(w http.ResponseWriter, r *http.Request) { userInfo, err := tailscale.WhoIs(r.Context(), r.RemoteAddr) if err != nil { http.Error(w, "can't get whois response: "+err.Error(), http.StatusInternalServerError) @@ -69,9 +90,104 @@ func (s Server) whois(w http.ResponseWriter, r *http.Request) { } w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(userInfo) } +func (s *Server) addQuery(whois *apitype.WhoIsResponse, w http.ResponseWriter, r *http.Request) { + type AddQueryRequest struct { + Name string `json:"name"` + Query string `json:"query"` + } + + var aqr AddQueryRequest + err := json.NewDecoder(r.Body).Decode(&aqr) + if err != nil { + ln.Error(r.Context(), err) + http.Error(w, "bad json", http.StatusBadRequest) + return + } + + who := whois.UserProfile.LoginName + + _, err = s.db.Exec("INSERT INTO sql_queries(name, query, who) VALUES (?, ?, ?)", aqr.Name, aqr.Query, who) + if err != nil { + ln.Error(r.Context(), err) + http.Error(w, "can't insert query", http.StatusInternalServerError) + return + } +} + +func (s *Server) getQueries(w http.ResponseWriter, r *http.Request) { + type SQLQuery struct { + Name string `json:"name"` + Query string `json:"query"` + Who string `json:"who"` + } + + var queries []SQLQuery + rows, err := s.db.QueryContext(r.Context(), "SELECT name, query, who FROM sql_queries") + if err != nil { + http.Error(w, "can't get query rows", http.StatusInternalServerError) + ln.Error(r.Context(), err) + return + } + defer rows.Close() + + for rows.Next() { + var sq SQLQuery + err := rows.Scan(&sq.Name, &sq.Query, &sq.Who) + if err != nil { + http.Error(w, "can't get query row", http.StatusInternalServerError) + ln.Error(r.Context(), err) + } + + queries = append(queries, sq) + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(queries) +} + +// curl https://twitchalitics.shark-harmonic.ts.net/api/importcsv -v -F csv=@"Channel Analytics and Revenue by day from Dec_19_2021 to Jan_17_2022.csv" +func (s *Server) importCSV(w http.ResponseWriter, r *http.Request) { + tx, err := s.db.BeginTx(r.Context(), nil) + if err != nil { + ln.Error(r.Context(), err) + http.Error(w, "can't hit db", http.StatusInternalServerError) + return + } + defer tx.Rollback() + + r.ParseMultipartForm(10 << 20) + file, handler, err := r.FormFile("csv") + if err != nil { + ln.Error(r.Context(), err) + http.Error(w, "can't read file", http.StatusInternalServerError) + } + + ln.WithF(r.Context(), ln.F{"filename": handler.Filename, "size": handler.Size}) + rows, err := ReadRows(file) + + for _, row := range rows { + err := row.Upsert(tx) + if err != nil { + ln.Error(r.Context(), err) + http.Error(w, "can't upsert row", http.StatusInternalServerError) + return + } + } + + err = tx.Commit() + if err != nil { + ln.Error(r.Context(), err) + http.Error(w, "can't commit data", http.StatusInternalServerError) + } + + http.Redirect(w, r, "/", http.StatusTemporaryRedirect) +} + func (s Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { userInfo, err := tailscale.WhoIs(r.Context(), r.RemoteAddr) if err != nil { diff --git a/static/importcsv.html b/static/importcsv.html new file mode 100644 index 0000000..c15c4f0 --- /dev/null +++ b/static/importcsv.html @@ -0,0 +1,15 @@ + + + + + + + Import CSV + + +
+ + +
+ + diff --git a/static/js/main.js b/static/js/main.js new file mode 100644 index 0000000..38951a6 --- /dev/null +++ b/static/js/main.js @@ -0,0 +1,4 @@ +import { h, g, x, r } from "./xeact.js"; +import { div, span, h1 } from "./xeact-html.js"; + + diff --git a/twitch.go b/twitch.go index 1695cdd..e107f94 100644 --- a/twitch.go +++ b/twitch.go @@ -19,7 +19,13 @@ func ImportCSV(ctx context.Context, db *sql.DB, dir string) error { } for _, fname := range files { - rows, err := ReadRowsFromFile(fname) + fin, err := os.Open(fname) + if err != nil { + return fmt.Errorf("can't open file: %w", err) + } + defer fin.Close() + + rows, err := ReadRows(fin) if err != nil { return err } @@ -132,18 +138,15 @@ func (t TwitchRevenueCSV) Upsert(tx *sql.Tx) error { return err } -func ReadRowsFromFile(fname string) ([]*TwitchRevenueCSV, error) { +func ReadRows(fin io.Reader) ([]*TwitchRevenueCSV, error) { var result []*TwitchRevenueCSV - fin, err := os.Open(fname) - if err != nil { - return nil, fmt.Errorf("can't open file: %w", err) - } - defer fin.Close() - rdr := csv.NewReader(fin) - _, err = rdr.Read() // discard header row + _, err := rdr.Read() // discard header row + if err != nil { + return nil, err + } var n = 1 for {