repos / pico

pico services - prose.sh, pastes.sh, imgs.sh, feeds.sh, pgs.sh
git clone https://github.com/picosh/pico.git

commit
77553f3
parent
828bab7
author
Eric Bower
date
2024-06-16 16:22:54 +0000 UTC
feat(prose): cli cmd `stats {post}` for analytics
4 files changed,  +255, -41
M pgs/cli.go
+3, -38
 1@@ -6,7 +6,6 @@ import (
 2 	"log/slog"
 3 	"path/filepath"
 4 	"strings"
 5-	"time"
 6 
 7 	"github.com/charmbracelet/lipgloss"
 8 	"github.com/charmbracelet/lipgloss/table"
 9@@ -196,40 +195,6 @@ func (c *Cmd) help() {
10 	c.output(getHelpText(c.Styles, c.User.Name, c.Width))
11 }
12 
13-func uniqueVisitorsTbl(intervals []*db.VisitInterval) *table.Table {
14-	headers := []string{"Date", "Unique Visitors"}
15-	data := [][]string{}
16-	for _, d := range intervals {
17-		data = append(data, []string{
18-			d.Interval.Format(time.RFC3339Nano),
19-			fmt.Sprintf("%d", d.Visitors),
20-		})
21-	}
22-
23-	t := table.New().
24-		Border(lipgloss.RoundedBorder()).
25-		Headers(headers...).
26-		Rows(data...)
27-	return t
28-}
29-
30-func visitUrlsTbl(urls []*db.VisitUrl) *table.Table {
31-	headers := []string{"Site", "Count"}
32-	data := [][]string{}
33-	for _, d := range urls {
34-		data = append(data, []string{
35-			d.Url,
36-			fmt.Sprintf("%d", d.Count),
37-		})
38-	}
39-
40-	t := table.New().
41-		Border(lipgloss.RoundedBorder()).
42-		Headers(headers...).
43-		Rows(data...)
44-	return t
45-}
46-
47 func (c *Cmd) statsByProject(projectName string) error {
48 	project, err := c.Dbpool.FindProjectByName(c.User.ID, projectName)
49 	if err != nil {
50@@ -249,14 +214,14 @@ func (c *Cmd) statsByProject(projectName string) error {
51 	}
52 
53 	c.output("Top URLs")
54-	topUrlsTbl := visitUrlsTbl(summary.TopUrls)
55+	topUrlsTbl := common.VisitUrlsTbl(summary.TopUrls)
56 	c.output(topUrlsTbl.Width(c.Width).String())
57 
58 	c.output("Top Referers")
59-	topRefsTbl := visitUrlsTbl(summary.TopReferers)
60+	topRefsTbl := common.VisitUrlsTbl(summary.TopReferers)
61 	c.output(topRefsTbl.Width(c.Width).String())
62 
63-	uniqueTbl := uniqueVisitorsTbl(summary.Intervals)
64+	uniqueTbl := common.UniqueVisitorsTbl(summary.Intervals)
65 	c.output("Unique Visitors this Month")
66 	c.output(uniqueTbl.Width(c.Width).String())
67 
A prose/cli.go
+198, -0
  1@@ -0,0 +1,198 @@
  2+package prose
  3+
  4+import (
  5+	"errors"
  6+	"fmt"
  7+	"log/slog"
  8+	"strings"
  9+
 10+	"github.com/charmbracelet/ssh"
 11+	"github.com/charmbracelet/wish"
 12+	bm "github.com/charmbracelet/wish/bubbletea"
 13+	"github.com/picosh/pico/db"
 14+	"github.com/picosh/pico/shared"
 15+	"github.com/picosh/pico/tui/common"
 16+)
 17+
 18+func getUser(s ssh.Session, dbpool db.DB) (*db.User, error) {
 19+	var err error
 20+	key, err := shared.KeyText(s)
 21+	if err != nil {
 22+		return nil, fmt.Errorf("key not found")
 23+	}
 24+
 25+	user, err := dbpool.FindUserForKey(s.User(), key)
 26+	if err != nil {
 27+		return nil, err
 28+	}
 29+
 30+	if user.Name == "" {
 31+		return nil, fmt.Errorf("must have username set")
 32+	}
 33+
 34+	return user, nil
 35+}
 36+
 37+type Cmd struct {
 38+	User    *db.User
 39+	Session shared.CmdSession
 40+	Log     *slog.Logger
 41+	Dbpool  db.DB
 42+	Styles  common.Styles
 43+	Width   int
 44+	Height  int
 45+}
 46+
 47+func (c *Cmd) output(out string) {
 48+	_, _ = c.Session.Write([]byte(out + "\r\n"))
 49+}
 50+
 51+func (c *Cmd) help() {
 52+	helpStr := "Commands: [help, stats]\n"
 53+	c.output(helpStr)
 54+}
 55+
 56+func (c *Cmd) statsByPost(postSlug string) error {
 57+	post, err := c.Dbpool.FindPostWithSlug(postSlug, c.User.ID, "prose")
 58+	if err != nil {
 59+		return errors.Join(err, fmt.Errorf("post (%s) does not exit", postSlug))
 60+	}
 61+
 62+	opts := &db.SummaryOpts{
 63+		FkID:     post.ID,
 64+		By:       "post_id",
 65+		Interval: "day",
 66+		Origin:   shared.StartOfMonth(),
 67+	}
 68+
 69+	summary, err := c.Dbpool.VisitSummary(opts)
 70+	if err != nil {
 71+		return err
 72+	}
 73+
 74+	c.output("Top URLs")
 75+	topUrlsTbl := common.VisitUrlsTbl(summary.TopUrls)
 76+	c.output(topUrlsTbl.Width(c.Width).String())
 77+
 78+	c.output("Top Referers")
 79+	topRefsTbl := common.VisitUrlsTbl(summary.TopReferers)
 80+	c.output(topRefsTbl.Width(c.Width).String())
 81+
 82+	uniqueTbl := common.UniqueVisitorsTbl(summary.Intervals)
 83+	c.output("Unique Visitors this Month")
 84+	c.output(uniqueTbl.Width(c.Width).String())
 85+
 86+	return nil
 87+}
 88+
 89+func (c *Cmd) stats() error {
 90+	opts := &db.SummaryOpts{
 91+		FkID:     c.User.ID,
 92+		By:       "user_id",
 93+		Interval: "day",
 94+		Origin:   shared.StartOfMonth(),
 95+		Where:    "AND (post_id IS NOT NULL OR (post_id IS NULL AND project_id IS NULL))",
 96+	}
 97+
 98+	summary, err := c.Dbpool.VisitSummary(opts)
 99+	if err != nil {
100+		return err
101+	}
102+
103+	c.output("Top URLs")
104+	topUrlsTbl := common.VisitUrlsTbl(summary.TopUrls)
105+	c.output(topUrlsTbl.Width(c.Width).String())
106+
107+	c.output("Top Referers")
108+	topRefsTbl := common.VisitUrlsTbl(summary.TopReferers)
109+	c.output(topRefsTbl.Width(c.Width).String())
110+
111+	uniqueTbl := common.UniqueVisitorsTbl(summary.Intervals)
112+	c.output("Unique Visitors this Month")
113+	c.output(uniqueTbl.Width(c.Width).String())
114+
115+	return nil
116+}
117+
118+type CliHandler struct {
119+	DBPool db.DB
120+	Logger *slog.Logger
121+}
122+
123+func WishMiddleware(handler *CliHandler) wish.Middleware {
124+	dbpool := handler.DBPool
125+	log := handler.Logger
126+
127+	return func(next ssh.Handler) ssh.Handler {
128+		return func(sesh ssh.Session) {
129+			args := sesh.Command()
130+			if len(args) == 0 {
131+				next(sesh)
132+				return
133+			}
134+
135+			// default width and height when no pty
136+			width := 80
137+			height := 24
138+			pty, _, ok := sesh.Pty()
139+			if ok {
140+				width = pty.Window.Width
141+				height = pty.Window.Height
142+			}
143+
144+			user, err := getUser(sesh, dbpool)
145+			if err != nil {
146+				wish.Errorln(sesh, err)
147+				return
148+			}
149+
150+			renderer := bm.MakeRenderer(sesh)
151+			styles := common.DefaultStyles(renderer)
152+
153+			opts := Cmd{
154+				Session: sesh,
155+				User:    user,
156+				Log:     log,
157+				Dbpool:  dbpool,
158+				Styles:  styles,
159+				Width:   width,
160+				Height:  height,
161+			}
162+
163+			cmd := strings.TrimSpace(args[0])
164+			if len(args) == 1 {
165+				if cmd == "help" {
166+					opts.help()
167+					return
168+				} else if cmd == "stats" {
169+					opts.stats()
170+					return
171+				} else {
172+					next(sesh)
173+					return
174+				}
175+			}
176+
177+			postSlug := strings.TrimSpace(args[1])
178+			cmdArgs := args[2:]
179+			log.Info(
180+				"pgs middleware detected command",
181+				"args", args,
182+				"cmd", cmd,
183+				"post", postSlug,
184+				"cmdArgs", cmdArgs,
185+			)
186+
187+			if cmd == "stats" {
188+				err := opts.statsByPost(postSlug)
189+				if err != nil {
190+					wish.Fatalln(sesh, err)
191+				}
192+				return
193+			} else {
194+				next(sesh)
195+				return
196+			}
197+		}
198+	}
199+}
M prose/ssh.go
+10, -3
 1@@ -32,7 +32,7 @@ func (me *SSHServer) authHandler(ctx ssh.Context, key ssh.PublicKey) bool {
 2 	return true
 3 }
 4 
 5-func createRouter(handler *filehandlers.FileHandlerRouter) proxy.Router {
 6+func createRouter(handler *filehandlers.FileHandlerRouter, cliHandler *CliHandler) proxy.Router {
 7 	return func(sh ssh.Handler, s ssh.Session) []wish.Middleware {
 8 		return []wish.Middleware{
 9 			pipe.Middleware(handler, ".md"),
10@@ -41,19 +41,20 @@ func createRouter(handler *filehandlers.FileHandlerRouter) proxy.Router {
11 			wishrsync.Middleware(handler),
12 			auth.Middleware(handler),
13 			wsh.PtyMdw(wsh.DeprecatedNotice()),
14+			WishMiddleware(cliHandler),
15 			wsh.LogMiddleware(handler.GetLogger()),
16 		}
17 	}
18 }
19 
20-func withProxy(handler *filehandlers.FileHandlerRouter, otherMiddleware ...wish.Middleware) ssh.Option {
21+func withProxy(handler *filehandlers.FileHandlerRouter, cliHandler *CliHandler, otherMiddleware ...wish.Middleware) ssh.Option {
22 	return func(server *ssh.Server) error {
23 		err := sftp.SSHOption(handler)(server)
24 		if err != nil {
25 			return err
26 		}
27 
28-		return proxy.WithProxy(createRouter(handler), otherMiddleware...)(server)
29+		return proxy.WithProxy(createRouter(handler, cliHandler), otherMiddleware...)(server)
30 	}
31 }
32 
33@@ -91,6 +92,11 @@ func StartSshServer() {
34 	handler := filehandlers.NewFileHandlerRouter(cfg, dbh, fileMap)
35 	handler.Spaces = []string{cfg.Space, "imgs"}
36 
37+	cliHandler := &CliHandler{
38+		Logger: logger,
39+		DBPool: dbh,
40+	}
41+
42 	sshServer := &SSHServer{}
43 	s, err := wish.NewServer(
44 		wish.WithAddress(fmt.Sprintf("%s:%s", host, port)),
45@@ -98,6 +104,7 @@ func StartSshServer() {
46 		wish.WithPublicKeyAuth(sshServer.authHandler),
47 		withProxy(
48 			handler,
49+			cliHandler,
50 			promwish.Middleware(fmt.Sprintf("%s:%s", host, promPort), "prose-ssh"),
51 		),
52 	)
A tui/common/tbl.go
+44, -0
 1@@ -0,0 +1,44 @@
 2+package common
 3+
 4+import (
 5+	"fmt"
 6+	"time"
 7+
 8+	"github.com/charmbracelet/lipgloss"
 9+	"github.com/charmbracelet/lipgloss/table"
10+	"github.com/picosh/pico/db"
11+)
12+
13+func UniqueVisitorsTbl(intervals []*db.VisitInterval) *table.Table {
14+	headers := []string{"Date", "Unique Visitors"}
15+	data := [][]string{}
16+	for _, d := range intervals {
17+		data = append(data, []string{
18+			d.Interval.Format(time.RFC3339Nano),
19+			fmt.Sprintf("%d", d.Visitors),
20+		})
21+	}
22+
23+	t := table.New().
24+		Border(lipgloss.RoundedBorder()).
25+		Headers(headers...).
26+		Rows(data...)
27+	return t
28+}
29+
30+func VisitUrlsTbl(urls []*db.VisitUrl) *table.Table {
31+	headers := []string{"Site", "Count"}
32+	data := [][]string{}
33+	for _, d := range urls {
34+		data = append(data, []string{
35+			d.Url,
36+			fmt.Sprintf("%d", d.Count),
37+		})
38+	}
39+
40+	t := table.New().
41+		Border(lipgloss.RoundedBorder()).
42+		Headers(headers...).
43+		Rows(data...)
44+	return t
45+}