parent
6da7a3d245
commit
84ca87befa
2
main.go
2
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)
|
||||
|
|
|
@ -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
|
||||
);
|
||||
|
|
118
server.go
118
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 {
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
|
||||
<title>Import CSV</title>
|
||||
</head>
|
||||
<body>
|
||||
<form enctype="multipart/form-data" action="/api/csvimport" method="post">
|
||||
<input type="file" name="csv" />
|
||||
<input type="submit" value="upload" />
|
||||
</form>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,4 @@
|
|||
import { h, g, x, r } from "./xeact.js";
|
||||
import { div, span, h1 } from "./xeact-html.js";
|
||||
|
||||
|
21
twitch.go
21
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 {
|
||||
|
|
Loading…
Reference in New Issue