parent
6da7a3d245
commit
84ca87befa
2
main.go
2
main.go
|
@ -81,6 +81,8 @@ func main() {
|
||||||
}
|
}
|
||||||
defer db.Close()
|
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)
|
done, tsLgr, err := getLogFout(*tsLogLoc)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ln.FatalErr(ctx, err)
|
ln.FatalErr(ctx, err)
|
||||||
|
|
|
@ -37,3 +37,9 @@ CREATE TABLE IF NOT EXISTS twitch_revenue
|
||||||
-- generated columns
|
-- 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)
|
, 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"
|
"context"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
|
"embed"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"tailscale.com/client/tailscale"
|
"tailscale.com/client/tailscale"
|
||||||
|
"tailscale.com/client/tailscale/apitype"
|
||||||
"tailscale.com/tsnet"
|
"tailscale.com/tsnet"
|
||||||
"within.website/ln"
|
"within.website/ln"
|
||||||
"within.website/ln/ex"
|
"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 {
|
type Server struct {
|
||||||
db *sql.DB
|
db *sql.DB
|
||||||
mux *http.ServeMux
|
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/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
|
return s
|
||||||
}
|
}
|
||||||
|
@ -60,7 +81,7 @@ func (s *Server) ListenAndServe() error {
|
||||||
return hs.Serve(l)
|
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)
|
userInfo, err := tailscale.WhoIs(r.Context(), r.RemoteAddr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, "can't get whois response: "+err.Error(), http.StatusInternalServerError)
|
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.Header().Set("Content-Type", "application/json")
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
json.NewEncoder(w).Encode(userInfo)
|
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) {
|
func (s Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
userInfo, err := tailscale.WhoIs(r.Context(), r.RemoteAddr)
|
userInfo, err := tailscale.WhoIs(r.Context(), r.RemoteAddr)
|
||||||
if err != nil {
|
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 {
|
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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -132,18 +138,15 @@ func (t TwitchRevenueCSV) Upsert(tx *sql.Tx) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func ReadRowsFromFile(fname string) ([]*TwitchRevenueCSV, error) {
|
func ReadRows(fin io.Reader) ([]*TwitchRevenueCSV, error) {
|
||||||
var result []*TwitchRevenueCSV
|
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)
|
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
|
var n = 1
|
||||||
|
|
||||||
for {
|
for {
|
||||||
|
|
Loading…
Reference in New Issue