repos / pico

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

commit
65b128e
parent
ca51aef
author
Eric Bower
date
2024-05-07 13:13:13 +0000 UTC
feat: senpai integration (#129)

Co-authored-by: Antonio Mika <antoniomika@gmail.com>
11 files changed,  +223, -16
M go.mod
M go.sum
M Dockerfile
+2, -0
 1@@ -56,10 +56,12 @@ ENTRYPOINT ["/app/web"]
 2 FROM scratch as release-ssh
 3 
 4 WORKDIR /app
 5+ENV TERM="xterm-256color"
 6 
 7 ARG APP=prose
 8 
 9 COPY --from=builder-ssh /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
10 COPY --from=builder-ssh /go/bin/${APP}-ssh ./ssh
11 
12+
13 ENTRYPOINT ["/app/ssh"]
M db/db.go
+2, -1
 1@@ -340,7 +340,8 @@ type DB interface {
 2 	FindUserForToken(token string) (*User, error)
 3 	FindTokensForUser(userID string) ([]*Token, error)
 4 	InsertToken(userID, name string) (string, error)
 5-	FindRssToken(userID string) (string, error)
 6+	UpsertToken(userID, name string) (string, error)
 7+	FindTokenByName(userID, name string) (string, error)
 8 	RemoveToken(tokenID string) error
 9 
10 	FindPosts() ([]*Post, error)
M db/postgres/storage.go
+16, -6
 1@@ -147,10 +147,10 @@ const (
 2 	FROM app_users
 3 	LEFT JOIN tokens ON tokens.user_id = app_users.id
 4 	WHERE tokens.token = $1 AND tokens.expires_at > NOW()`
 5-	sqlInsertToken           = `INSERT INTO tokens (user_id, name) VALUES($1, $2) RETURNING token;`
 6-	sqlRemoveToken           = `DELETE FROM tokens WHERE id = $1`
 7-	sqlSelectTokensForUser   = `SELECT id, user_id, name, created_at, expires_at FROM tokens WHERE user_id = $1`
 8-	sqlSelectRssTokenForUser = `SELECT token FROM tokens WHERE user_id = $1 AND name = 'pico-rss'`
 9+	sqlInsertToken              = `INSERT INTO tokens (user_id, name) VALUES($1, $2) RETURNING token;`
10+	sqlRemoveToken              = `DELETE FROM tokens WHERE id = $1`
11+	sqlSelectTokensForUser      = `SELECT id, user_id, name, created_at, expires_at FROM tokens WHERE user_id = $1`
12+	sqlSelectTokenByNameForUser = `SELECT token FROM tokens WHERE user_id = $1 AND name = $2`
13 
14 	sqlSelectTotalUsers          = `SELECT count(id) FROM app_users`
15 	sqlSelectUsersAfterDate      = `SELECT count(id) FROM app_users WHERE created_at >= $1`
16@@ -1789,9 +1789,19 @@ func (me *PsqlDB) InsertToken(userID, name string) (string, error) {
17 	return token, nil
18 }
19 
20-func (me *PsqlDB) FindRssToken(userID string) (string, error) {
21+func (me *PsqlDB) UpsertToken(userID, name string) (string, error) {
22+	token, _ := me.FindTokenByName(userID, name)
23+	if token != "" {
24+		return token, nil
25+	}
26+
27+	token, err := me.InsertToken(userID, name)
28+	return token, err
29+}
30+
31+func (me *PsqlDB) FindTokenByName(userID, name string) (string, error) {
32 	var token string
33-	err := me.Db.QueryRow(sqlSelectRssTokenForUser, userID).Scan(&token)
34+	err := me.Db.QueryRow(sqlSelectTokenByNameForUser, userID, name).Scan(&token)
35 	if err != nil {
36 		return "", err
37 	}
M go.mod
+13, -0
 1@@ -9,7 +9,12 @@ replace github.com/charmbracelet/wish => github.com/charmbracelet/wish v1.2.0
 2 
 3 replace github.com/charmbracelet/ssh => github.com/charmbracelet/ssh v0.0.0-20230822194956-1a051f898e09
 4 
 5+replace git.sr.ht/~delthas/senpai => github.com/picosh/senpai v0.0.0-20240503200611-af89e73973b0
 6+
 7+replace github.com/gdamore/tcell/v2 => github.com/delthas/tcell/v2 v2.4.1-0.20230710100648-1489e78d90fb
 8+
 9 require (
10+	git.sr.ht/~delthas/senpai v0.3.1-0.20240425235039-206be659439e
11 	github.com/alecthomas/chroma v0.10.0
12 	github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de
13 	github.com/charmbracelet/bubbles v0.16.1
14@@ -47,6 +52,7 @@ require (
15 )
16 
17 require (
18+	git.sr.ht/~emersion/go-scfg v0.0.0-20231004133111-9dce55c8d63b // indirect
19 	github.com/DavidGamba/go-getoptions v0.29.0 // indirect
20 	github.com/PuerkitoBio/goquery v1.8.1 // indirect
21 	github.com/andybalholm/cascadia v1.3.2 // indirect
22@@ -61,6 +67,8 @@ require (
23 	github.com/charmbracelet/log v0.3.1 // indirect
24 	github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 // indirect
25 	github.com/creack/pty v1.1.21 // indirect
26+	github.com/delthas/go-libnp v0.0.0-20221222161248-0e45ece1f878 // indirect
27+	github.com/delthas/go-localeinfo v0.0.0-20221116001557-686a1e185118 // indirect
28 	github.com/dlclark/regexp2 v1.10.0 // indirect
29 	github.com/dsoprea/go-exif v0.0.0-20230826092837-6579e82b732d // indirect
30 	github.com/dsoprea/go-exif/v2 v2.0.0-20230826092837-6579e82b732d // indirect
31@@ -71,10 +79,13 @@ require (
32 	github.com/dsoprea/go-utility v0.0.0-20221003172846-a3e1774ef349 // indirect
33 	github.com/dustin/go-humanize v1.0.1 // indirect
34 	github.com/forPelevin/gomoji v1.1.3 // indirect
35+	github.com/gdamore/encoding v1.0.0 // indirect
36+	github.com/gdamore/tcell/v2 v2.6.1-0.20230327043120-47ec3a77754f // indirect
37 	github.com/go-errors/errors v1.5.1 // indirect
38 	github.com/go-logfmt/logfmt v0.6.0 // indirect
39 	github.com/go-ole/go-ole v1.3.0 // indirect
40 	github.com/go-xmlfmt/xmlfmt v1.1.2 // indirect
41+	github.com/godbus/dbus/v5 v5.1.0 // indirect
42 	github.com/golang-jwt/jwt/v4 v4.5.0 // indirect
43 	github.com/golang/geo v0.0.0-20230421003525-6adc56603217 // indirect
44 	github.com/golang/protobuf v1.5.3 // indirect
45@@ -129,6 +140,8 @@ require (
46 	golang.org/x/sys v0.16.0 // indirect
47 	golang.org/x/term v0.16.0 // indirect
48 	golang.org/x/text v0.14.0 // indirect
49+	golang.org/x/time v0.4.0 // indirect
50 	google.golang.org/protobuf v1.31.0 // indirect
51 	gopkg.in/ini.v1 v1.67.0 // indirect
52+	mvdan.cc/xurls/v2 v2.5.0 // indirect
53 )
M go.sum
+28, -0
  1@@ -1,3 +1,5 @@
  2+git.sr.ht/~emersion/go-scfg v0.0.0-20231004133111-9dce55c8d63b h1:Lf4oYBOJVmbYzrfqWfXUvSpXQPNMgnbN0efn5A7bH3M=
  3+git.sr.ht/~emersion/go-scfg v0.0.0-20231004133111-9dce55c8d63b/go.mod h1:ybgvEJTIx5XbaspSviB3KNa6OdPmAZqDoSud7z8fFlw=
  4 github.com/DavidGamba/go-getoptions v0.29.0 h1:cU8MjOyfAyPZke4hrgEuiGBJHS9PFYPAHve2fhDhdDk=
  5 github.com/DavidGamba/go-getoptions v0.29.0/go.mod h1:zE97E3PR9P3BI/HKyNYgdMlYxodcuiC6W68KIgeYT84=
  6 github.com/PuerkitoBio/goquery v1.8.1 h1:uQxhNlArOIdbrH1tr0UXwdVFgDcZDrZVdcpygAcwmWM=
  7@@ -47,6 +49,12 @@ github.com/creack/pty v1.1.21/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr
  8 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
  9 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
 10 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 11+github.com/delthas/go-libnp v0.0.0-20221222161248-0e45ece1f878 h1:v8W8eW7eb2bHFXBA80UKcoe0TvEu46NlTHSDRvgAbMU=
 12+github.com/delthas/go-libnp v0.0.0-20221222161248-0e45ece1f878/go.mod h1:aGVXnhWpDlt5U4SphG97o1gszctZKvBTXy320E8Buw4=
 13+github.com/delthas/go-localeinfo v0.0.0-20221116001557-686a1e185118 h1:Xzf9ra1QRJXD62gwudjI2iBq7x9CusvHd83Dg2OnUmE=
 14+github.com/delthas/go-localeinfo v0.0.0-20221116001557-686a1e185118/go.mod h1:sG54BxlyQgIskYURLrg7mvhoGBe0Qq12DNtYRALwNa4=
 15+github.com/delthas/tcell/v2 v2.4.1-0.20230710100648-1489e78d90fb h1:x0hrYPzXpmn3L/4QnW0UXJnHX9oz0OcZNcsSgregusw=
 16+github.com/delthas/tcell/v2 v2.4.1-0.20230710100648-1489e78d90fb/go.mod h1:be9omFATkdr0D9qewWW3d+MEvl5dha+Etb5y65J2H8Y=
 17 github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
 18 github.com/dlclark/regexp2 v1.10.0 h1:+/GIL799phkJqYW+3YbOd8LCcbHzT0Pbo8zl70MHsq0=
 19 github.com/dlclark/regexp2 v1.10.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
 20@@ -78,6 +86,8 @@ github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkp
 21 github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
 22 github.com/forPelevin/gomoji v1.1.3 h1:7c3dYzVmYhpOL3bS4riXqSWJBX3BhSvH68yoNNf3FH0=
 23 github.com/forPelevin/gomoji v1.1.3/go.mod h1:ypB7Kz3Fsp+LVR7KoT7mEFOioYBuTuAtaAT4RGl+ASY=
 24+github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko=
 25+github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg=
 26 github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
 27 github.com/go-errors/errors v1.0.2/go.mod h1:psDX2osz5VnTOnFWbDeWwS7yejl+uV3FEWEp4lssFEs=
 28 github.com/go-errors/errors v1.1.1/go.mod h1:psDX2osz5VnTOnFWbDeWwS7yejl+uV3FEWEp4lssFEs=
 29@@ -91,6 +101,8 @@ github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW
 30 github.com/go-xmlfmt/xmlfmt v0.0.0-20191208150333-d5b6f63a941b/go.mod h1:aUCEOzzezBEjDBbFBoSiya/gduyIiWYRP6CnSFIV8AM=
 31 github.com/go-xmlfmt/xmlfmt v1.1.2 h1:Nea7b4icn8s57fTx1M5AI4qQT5HEM3rVUO8MuE6g80U=
 32 github.com/go-xmlfmt/xmlfmt v1.1.2/go.mod h1:aUCEOzzezBEjDBbFBoSiya/gduyIiWYRP6CnSFIV8AM=
 33+github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
 34+github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
 35 github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg=
 36 github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
 37 github.com/golang/geo v0.0.0-20190916061304-5b978397cfec/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI=
 38@@ -146,6 +158,7 @@ github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2J
 39 github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
 40 github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
 41 github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
 42+github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
 43 github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
 44 github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
 45 github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo=
 46@@ -193,6 +206,8 @@ github.com/picosh/ptun v0.0.0-20240417140706-811cc2b70d9a h1:sBqfT6KBIYllVaw4bT1
 47 github.com/picosh/ptun v0.0.0-20240417140706-811cc2b70d9a/go.mod h1:WXCrhe0l9VL3ji0pdhvSJD6LLx99rJoAA/+PUQXf0Mo=
 48 github.com/picosh/send v0.0.0-20240217194807-77b972121e63 h1:VSSbAejFzj2KBThfVnMcNXQwzHmwjPUridgi29LxihU=
 49 github.com/picosh/send v0.0.0-20240217194807-77b972121e63/go.mod h1:1JCq0NVOdTDenQ0/Kd8e4rP80lu06UHJJ+6dQxhcpew=
 50+github.com/picosh/senpai v0.0.0-20240503200611-af89e73973b0 h1:pBRIbiCj7K6rGELijb//dYhyCo8A3fvxW5dijrJVtjs=
 51+github.com/picosh/senpai v0.0.0-20240503200611-af89e73973b0/go.mod h1:QaBDtybFC5gz7EG/9c3bgzuyW7W5W2rYLFZxWNuWk3Q=
 52 github.com/pkg/sftp v1.13.6 h1:JFZT4XbOU7l77xGSpOdW+pwIMqP044IyjXX6FGyEKFo=
 53 github.com/pkg/sftp v1.13.6/go.mod h1:tz1ryNURKu77RL+GuCzmoJYxQczL3wLNNpPWagdg4Qk=
 54 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 55@@ -213,8 +228,10 @@ github.com/prometheus/prom2json v1.3.3 h1:IYfSMiZ7sSOfliBoo89PcufjWO4eAR0gznGcET
 56 github.com/prometheus/prom2json v1.3.3/go.mod h1:Pv4yIPktEkK7btWsrUTWDDDrnpUrAELaOCj+oFwlgmc=
 57 github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
 58 github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
 59+github.com/rivo/uniseg v0.4.3/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
 60 github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis=
 61 github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
 62+github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
 63 github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
 64 github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
 65 github.com/rs/xid v1.5.0 h1:mKX4bl4iPYJtEIxp6CYiUuLQ/8DYMoz0PUdtGgMFRVc=
 66@@ -281,12 +298,14 @@ golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPh
 67 golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
 68 golang.org/x/crypto v0.0.0-20220826181053-bd7e27e6170d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
 69 golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw=
 70+golang.org/x/crypto v0.15.0/go.mod h1:4ChreQoLWfG3xLDer1WdlH5NdlQ3+mwnQq1YTKY+72g=
 71 golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc=
 72 golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
 73 golang.org/x/exp v0.0.0-20231108232855-2478ac86f678 h1:mchzmB1XO2pMaKFRqk/+MV3mgGG96aqaPXaMifQU47w=
 74 golang.org/x/exp v0.0.0-20231108232855-2478ac86f678/go.mod h1:zk2irFbV9DP96SEBUUAy67IdHUaZuSnrz1n472HUCLE=
 75 golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
 76 golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
 77+golang.org/x/mod v0.10.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
 78 golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
 79 golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 80 golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 81@@ -303,6 +322,8 @@ golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
 82 golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
 83 golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
 84 golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
 85+golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
 86+golang.org/x/net v0.18.0/go.mod h1:/czyP5RqHAH4odGYxBJ1qz0+CE5WZ+2j1YgoEo8F2jQ=
 87 golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c=
 88 golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U=
 89 golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 90@@ -330,6 +351,7 @@ golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 91 golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 92 golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 93 golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 94+golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
 95 golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
 96 golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
 97 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 98@@ -337,6 +359,8 @@ golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuX
 99 golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
100 golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
101 golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
102+golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
103+golang.org/x/term v0.14.0/go.mod h1:TySc+nGkYR6qt8km8wUhuFRTVSMIX3XPR58y2lC8vww=
104 golang.org/x/term v0.16.0 h1:m+B6fahuftsE9qjo0VWp2FW0mB3MTJvR0BaMQrq0pmE=
105 golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY=
106 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
107@@ -348,6 +372,8 @@ golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
108 golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
109 golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
110 golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
111+golang.org/x/time v0.4.0 h1:Z81tqI5ddIoXDPvVQ7/7CC9TnLM7ubaFG2qXYd5BbYY=
112+golang.org/x/time v0.4.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
113 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
114 golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
115 golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
116@@ -370,5 +396,7 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
117 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
118 gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
119 gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
120+mvdan.cc/xurls/v2 v2.5.0 h1:lyBNOm8Wo71UknhUs4QTFUNNMyxy2JEIaKKo0RWOh+8=
121+mvdan.cc/xurls/v2 v2.5.0/go.mod h1:yQgaGQ1rFtJUzkmKiHYSSfuQxqfYmd//X6PxvholpeE=
122 pgregory.net/rapid v1.1.0 h1:CMa0sjHSru3puNx+J0MIAuiiEV4N0qj8/cMWGBBCsjw=
123 pgregory.net/rapid v1.1.0/go.mod h1:PY5XlDGj0+V1FCq0o192FdRhpKHGTRIWBgqjDBTrq04=
M pastes/public/smol.css
+14, -0
 1@@ -488,6 +488,10 @@ figure {
 2   margin-left: 0.5rem;
 3 }
 4 
 5+.pt-0 {
 6+  padding-top: 0;
 7+}
 8+
 9 .my {
10   margin-top: 0.5rem;
11   margin-bottom: 0.5rem;
12@@ -572,6 +576,12 @@ figure {
13   gap: 0.5rem;
14 }
15 
16+.group-2 {
17+  display: flex;
18+  flex-direction: column;
19+  gap: 1rem;
20+}
21+
22 .group-h {
23   display: flex;
24   gap: 0.5rem;
25@@ -710,4 +720,8 @@ figure {
26   header {
27     margin: 0;
28   }
29+
30+  .flex-collapse {
31+    flex-direction: column;
32+  }
33 }
M pico/cli.go
+26, -8
 1@@ -123,8 +123,8 @@ func WishMiddleware(handler *CliHandler) wish.Middleware {
 2 
 3 	return func(next ssh.Handler) ssh.Handler {
 4 		return func(sesh ssh.Session) {
 5-			_, _, activePty := sesh.Pty()
 6-			if activePty {
 7+			args := sesh.Command()
 8+			if len(args) == 0 {
 9 				next(sesh)
10 				return
11 			}
12@@ -135,7 +135,30 @@ func WishMiddleware(handler *CliHandler) wish.Middleware {
13 				return
14 			}
15 
16-			args := sesh.Command()
17+			if len(args) > 0 && args[0] == "chat" {
18+				_, _, hasPty := sesh.Pty()
19+				if !hasPty {
20+					wish.Fatalln(
21+						sesh,
22+						"In order to render chat you need to enable PTY with the `ssh -t` flag",
23+					)
24+					return
25+				}
26+
27+				pass, err := dbpool.UpsertToken(user.ID, "pico-chat")
28+				if err != nil {
29+					wish.Fatalln(sesh, err)
30+					return
31+				}
32+				app, err := shared.NewSenpaiApp(sesh, user.Name, pass)
33+				if err != nil {
34+					wish.Fatalln(sesh, err)
35+					return
36+				}
37+				app.Run()
38+				app.Close()
39+				return
40+			}
41 
42 			opts := Cmd{
43 				Session: sesh,
44@@ -145,11 +168,6 @@ func WishMiddleware(handler *CliHandler) wish.Middleware {
45 				Write:   false,
46 			}
47 
48-			if len(args) == 0 {
49-				next(sesh)
50-				return
51-			}
52-
53 			cmd := strings.TrimSpace(args[0])
54 			if len(args) == 1 {
55 				if cmd == "help" {
M prose/public/smol.css
+14, -0
 1@@ -488,6 +488,10 @@ figure {
 2   margin-left: 0.5rem;
 3 }
 4 
 5+.pt-0 {
 6+  padding-top: 0;
 7+}
 8+
 9 .my {
10   margin-top: 0.5rem;
11   margin-bottom: 0.5rem;
12@@ -572,6 +576,12 @@ figure {
13   gap: 0.5rem;
14 }
15 
16+.group-2 {
17+  display: flex;
18+  flex-direction: column;
19+  gap: 1rem;
20+}
21+
22 .group-h {
23   display: flex;
24   gap: 0.5rem;
25@@ -710,4 +720,8 @@ figure {
26   header {
27     margin: 0;
28   }
29+
30+  .flex-collapse {
31+    flex-direction: column;
32+  }
33 }
A shared/senpai.go
+62, -0
 1@@ -0,0 +1,62 @@
 2+package shared
 3+
 4+import (
 5+	"git.sr.ht/~delthas/senpai"
 6+	"github.com/charmbracelet/ssh"
 7+)
 8+
 9+type Vtty struct {
10+	ssh.Session
11+}
12+
13+func (v Vtty) Drain() error {
14+	_, err := v.Write([]byte("\033[?25h\033[0 q\033[34h\033[?25h\033[39;49m\033[m^O\033[H\033[J\033[?1049l\033[?1l\033>\033[?1000l\033[?1002l\033[?1003l\033[?1006l\033[?2004l"))
15+	if err != nil {
16+		return err
17+	}
18+
19+	err = v.Exit(0)
20+	if err != nil {
21+		return err
22+	}
23+
24+	err = v.Close()
25+	return err
26+}
27+
28+func (v Vtty) Start() error {
29+	return nil
30+}
31+
32+func (v Vtty) Stop() error {
33+	return nil
34+}
35+
36+func (v Vtty) WindowSize() (width int, height int, err error) {
37+	pty, _, _ := v.Pty()
38+	return pty.Window.Width, pty.Window.Height, nil
39+}
40+
41+func (v Vtty) NotifyResize(cb func()) {
42+	_, notify, _ := v.Pty()
43+	go func() {
44+		for range notify {
45+			cb()
46+		}
47+	}()
48+}
49+
50+func NewSenpaiApp(sesh ssh.Session, username, pass string) (*senpai.App, error) {
51+	vty := Vtty{
52+		sesh,
53+	}
54+	senpaiCfg := senpai.Defaults()
55+	senpaiCfg.TLS = true
56+	senpaiCfg.Addr = "irc.pico.sh:6697"
57+	senpaiCfg.Nick = username
58+	senpaiCfg.Password = &pass
59+	senpaiCfg.Tty = vty
60+
61+	app, err := senpai.NewApp(senpaiCfg)
62+	return app, err
63+}
M tui/cms.go
+45, -0
  1@@ -3,6 +3,7 @@ package tui
  2 import (
  3 	"errors"
  4 	"fmt"
  5+	"io"
  6 
  7 	"github.com/charmbracelet/bubbles/spinner"
  8 	tea "github.com/charmbracelet/bubbletea"
  9@@ -30,6 +31,7 @@ const (
 10 	statusNoAccount
 11 	statusBrowsingKeys
 12 	statusBrowsingTokens
 13+	statusChat
 14 	statusQuitting
 15 )
 16 
 17@@ -50,6 +52,7 @@ type menuChoice int
 18 const (
 19 	keysChoice menuChoice = iota
 20 	tokensChoice
 21+	chatChoice
 22 	exitChoice
 23 	unsetChoice // set when no choice has been made
 24 )
 25@@ -58,6 +61,7 @@ const (
 26 var menuChoices = map[menuChoice]string{
 27 	keysChoice:   "Manage keys",
 28 	tokensChoice: "Manage tokens",
 29+	chatChoice:   "Chat",
 30 	exitChoice:   "Exit",
 31 }
 32 
 33@@ -87,6 +91,7 @@ func CmsMiddleware(cfg *shared.ConfigSite) bm.Handler {
 34 		styles := common.DefaultStyles(renderer)
 35 
 36 		m := model{
 37+			session:    s,
 38 			cfg:        cfg,
 39 			publicKey:  s.PublicKey(),
 40 			dbpool:     dbpool,
 41@@ -134,6 +139,7 @@ type model struct {
 42 	tokens          tokens.Model
 43 	createAccount   account.CreateModel
 44 	terminalSize    tea.WindowSizeMsg
 45+	session         ssh.Session
 46 }
 47 
 48 func (m model) Init() tea.Cmd {
 49@@ -311,6 +317,10 @@ func updateChildren(msg tea.Msg, m model) (model, tea.Cmd) {
 50 		m.status = statusBrowsingTokens
 51 		m.menuChoice = unsetChoice
 52 		cmd = tokens.LoadKeys(m.tokens)
 53+	case chatChoice:
 54+		m.status = statusChat
 55+		m.menuChoice = unsetChoice
 56+		cmd = m.loadChat()
 57 	case exitChoice:
 58 		m.status = statusQuitting
 59 		m.dbpool.Close()
 60@@ -320,6 +330,41 @@ func updateChildren(msg tea.Msg, m model) (model, tea.Cmd) {
 61 	return m, cmd
 62 }
 63 
 64+type SenpaiCmd struct {
 65+	user    *db.User
 66+	session ssh.Session
 67+	dbpool  db.DB
 68+}
 69+
 70+func (m *SenpaiCmd) Run() error {
 71+	pass, err := m.dbpool.UpsertToken(m.user.ID, "pico-chat")
 72+	if err != nil {
 73+		return err
 74+	}
 75+	app, err := shared.NewSenpaiApp(m.session, m.user.Name, pass)
 76+	if err != nil {
 77+		return err
 78+	}
 79+	app.Run()
 80+	app.Close()
 81+	return nil
 82+}
 83+
 84+func (m *SenpaiCmd) SetStdin(io.Reader)  {}
 85+func (m *SenpaiCmd) SetStdout(io.Writer) {}
 86+func (m *SenpaiCmd) SetStderr(io.Writer) {}
 87+
 88+func (m model) loadChat() tea.Cmd {
 89+	sp := &SenpaiCmd{
 90+		session: m.session,
 91+		dbpool:  m.dbpool,
 92+		user:    m.user,
 93+	}
 94+	return tea.Exec(sp, func(err error) tea.Msg {
 95+		return tea.Quit()
 96+	})
 97+}
 98+
 99 func (m model) menuView() string {
100 	var s string
101 	for i := 0; i < len(menuChoices); i++ {
M ui/api.go
+1, -1
1@@ -105,7 +105,7 @@ func findOrCreateRssToken(apiConfig *shared.ApiConfig, ctx ssh.Context) http.Han
2 
3 		dbpool := shared.GetDB(r)
4 		var err error
5-		rssToken, _ := dbpool.FindRssToken(user.ID)
6+		rssToken, _ := dbpool.FindTokenByName(user.ID, "pico-rss")
7 		if rssToken == "" {
8 			rssToken, err = dbpool.InsertToken(user.ID, "pico-rss")
9 			if err != nil {