Merge branch 'main' of ssh://git.bullercodeworks.com:2200/brian/expds

This commit is contained in:
2026-02-12 06:13:56 -06:00
10 changed files with 954 additions and 33 deletions

View File

@@ -23,8 +23,10 @@ package app
import (
"fmt"
"log/slog"
"net/url"
"strings"
"time"
"git.bullercodeworks.com/brian/expds/data"
"git.bullercodeworks.com/brian/expds/data/models"
@@ -44,7 +46,6 @@ type ScreenHome struct {
style tcell.Style
alert *w.Alert
showAlert bool
menuLayout *w.TopMenuLayout
@@ -215,9 +216,6 @@ func (s *ScreenHome) Draw() {
s.a.DrawWidget(s.loading)
}
s.a.DrawWidget(s.status)
if s.showAlert {
s.a.DrawWidget(s.alert)
}
}
func (s *ScreenHome) Exit() error { return nil }
func (s *ScreenHome) Log(t string, a ...any) {
@@ -318,9 +316,11 @@ func (s *ScreenHome) initCli() {
func (s *ScreenHome) toggleCli() {
if s.layout.Contains(s.cli) {
s.layout.Delete(s.cli)
s.layout.ActivateWidget(s.columns)
} else {
s.layout.Add(s.cli)
s.cli.SetVisible(true)
s.layout.ActivateWidget(s.cli)
}
}
func (s *ScreenHome) hideCli() {
@@ -346,7 +346,13 @@ func (s *ScreenHome) cliGetPds(args ...string) bool {
}
go func() {
defer func() { s.isLoading = false }()
pds, err := s.r.GetPDS(args[1])
var pds *models.Pds
var err error
if s.activePds != nil && string(s.activePds.AtId) == args[1] {
pds, err = s.r.ReloadPds(args[1])
} else {
pds, err = s.r.GetPDS(args[1])
}
if err != nil {
s.Log(err.Error())
return
@@ -355,7 +361,7 @@ func (s *ScreenHome) cliGetPds(args ...string) bool {
return
}
s.doOpen = false
s.cli.Log("Retrieved: %s (%s)", pds.AtId, pds.Did)
slog.Default().Debug(fmt.Sprintf("Retrieved: %s (%s) at %s", pds.AtId, pds.Did, time.Now().Format(time.RFC3339)))
s.activePds = pds
s.updatePdsListing()
n, err := s.pdsListing.GetActiveNode()
@@ -425,11 +431,14 @@ func (s *ScreenHome) cliSendStatus(args ...string) bool {
s.isLoading = true
go func() {
defer func() { s.isLoading = false }()
if !s.r.Auth.HasAuth() {
if !s.r.Auth.HasAuth(s.activePds.Did) {
s.Log("Not authorized. Run `authpds`")
return
}
s.r.SendToPDS()
err := s.r.SendToPDS(s.activePds.Did)
if err != nil {
s.Log("Error sending status: %w", err)
}
}()
return true
}
@@ -438,6 +447,7 @@ func (s *ScreenHome) updatePdsListing() {
s.pdsListing.SetTitle(fmt.Sprintf("─ %s (%s)", s.activePds.AtId.String(), s.activePds.Did.String()))
s.pdsListing.Clear()
nsidList := s.activePds.NSIDStringList()
var tree []*wd.TreeNode
for i, v := range nsidList {
t := wd.NewTreeNode(v, v)
nsid := s.activePds.NSIDs[i]
@@ -460,8 +470,9 @@ func (s *ScreenHome) updatePdsListing() {
c := wd.NewTreeNode(label, rIds[j])
t.AddChild(c)
}
s.pdsListing.Add(t)
tree = append(tree, t)
}
s.pdsListing.SetTree(tree)
s.layout.ActivateWidget(s.columns)
s.columns.ActivateWidget(s.pdsListing)
s.pdsListing.SetActive(true)

531
app/screen_home.go.orig Normal file
View File

@@ -0,0 +1,531 @@
/*
Copyright © Brian Buller <brian@bullercodeworks.com>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
*/
package app
import (
"fmt"
"log/slog"
"net/url"
"strings"
"time"
"git.bullercodeworks.com/brian/expds/data"
"git.bullercodeworks.com/brian/expds/data/models"
wd "git.bullercodeworks.com/brian/expds/widgets"
w "git.bullercodeworks.com/brian/tcell-widgets"
"github.com/bluesky-social/indigo/atproto/syntax"
"github.com/gdamore/tcell"
"github.com/skratchdot/open-golang/open"
"github.com/spf13/viper"
"golang.design/x/clipboard"
)
type ScreenHome struct {
a *App
r *data.Repo
w, h int
style tcell.Style
alert *w.Alert
menuLayout *w.TopMenuLayout
openPdsEntry *w.Field
layout *w.LinearLayout
columns *w.LinearLayout
activePds *models.Pds
pdsListing *wd.TreeBrowser
jsonContent *wd.JsonContent
status *wd.StatusBar
stPathBlock *wd.StatusBlock
cli *w.Cli
doOpen bool
isLoading bool
loading *w.Spinner
cursor int
}
func (s *ScreenHome) Init(a *App) {
s.a, s.r = a, a.repo
s.r.SetLogFunc(s.Log)
s.style = a.style
s.alert = w.NewAlert("expds.alert", s.style)
s.openPdsEntry = w.NewField("home.openpds.field", s.style)
s.openPdsEntry.SetLabel("ID")
s.openPdsEntry.SetActive(true)
s.openPdsEntry.SetPos(w.Coord{X: 3, Y: 1})
s.openPdsEntry.SetSize(w.Coord{X: 20, Y: 1})
s.doOpen = true
s.isLoading = false
s.loading = w.NewSpinner("home.loading", s.style)
s.loading.SetVisible(true)
s.loading.SetPos(w.Coord{X: 2, Y: 1})
s.menuLayout = w.NewTopMenuLayout("home.toplayout", s.style)
s.initMenu()
s.cli = w.NewCli("home.cli", s.style)
s.initCli()
s.cli.SetVisible(false)
s.layout = w.NewLinearLayout("home.layout", s.style)
s.columns = w.NewLinearLayout("home.layout.columns", s.style)
s.columns.SetOrientation(w.LinLayH)
//s.pdsListing = w.NewSimpleListWithHelp("pdslisting", s.style)
s.pdsListing = wd.NewTreeBrowser("pdslisting", s.style)
s.pdsListing.SetBorder([]rune{'─', '┬', '│', '┴', '─', '└', '│', '┌', '├', '─', '┤', '┬', '│', '┴', '┼'})
s.pdsListing.SetTitle(strings.Repeat(" ", 30))
s.pdsListing.SetOnSelect(s.selectPdsListingEntry)
s.pdsListing.SetOnChange(s.changePdsList)
s.pdsListing.SetVimMode(viper.GetBool(data.KeyVimMode))
s.pdsListing.SetLogger(s.Log)
s.jsonContent = wd.NewJsonContent("jsoncontent", s.style)
km := s.jsonContent.GetKeyMap()
km.Add(
w.NewKey(w.BuildEK(tcell.KeyEnter), func(ev *tcell.EventKey) bool {
// Init returns an error if the package is not ready for use.
err := clipboard.Init()
if err != nil {
s.Log("Error initializing clipboard: %s", err.Error())
return true
}
clipboard.Write(clipboard.FmtText, []byte(s.jsonContent.GetSelectedValue()))
return true
}),
w.NewKey(w.BuildEKr('O'), func(ev *tcell.EventKey) bool {
url := fmt.Sprintf("https://%s/xrpc/com.atproto.sync.getBlob?did=did:plc:pqwuemo2ic5tqmpwrajb2phi&cid=%s", s.activePds.AtId.String(), s.jsonContent.GetSelectedValue())
open.Run(url)
return true
}),
)
s.jsonContent.SetKeyMap(km)
s.jsonContent.SetBorder(
[]rune{'─', '┐', '│', '┘', '─', '─', ' ', '─', '├', '─', '┤', '┬', '│', '┴', '┼'},
)
statusStyle := s.style.Background(tcell.ColorDarkSlateGray)
s.status = wd.NewStatusBar("home.statusbar", statusStyle)
s.status.SetPos(w.Coord{X: 0, Y: s.a.GetH() - 1})
s.status.SetLogger(s.Log)
s.stPathBlock = wd.NewStatusBlock("home.statusbar.block", statusStyle.Foreground(tcell.ColorDarkSlateGray).Background(tcell.ColorOrange))
s.stPathBlock.SetType(wd.SBTypePath)
s.stPathBlock.SetParts([]string{"No PDS Loaded"})
s.status.Add(s.stPathBlock)
s.columns.AddAll(s.pdsListing, s.jsonContent)
s.layout.AddAll(s.columns)
s.layout.SetLogger(s.Log)
s.layout.SetWeight(s.columns, 4)
s.menuLayout.SetWidget(s.layout)
s.layout.SetLogger(s.Log)
s.columns.SetLogger(s.Log)
}
func (s *ScreenHome) GetName() string { return "home" }
func (s *ScreenHome) HandleResize(ev *tcell.EventResize) {
s.w, s.h = ev.Size()
s.menuLayout.HandleResize(w.Coord{X: s.w, Y: s.h - 1}.ResizeEvent())
s.alert.HandleResize(ev)
s.status.SetPos(w.Coord{X: 0, Y: s.h - 1})
s.status.HandleResize(w.Coord{X: s.w, Y: 1}.ResizeEvent())
}
func (s *ScreenHome) HandleKey(ev *tcell.EventKey) bool {
<<<<<<< HEAD
if s.showAlert {
handled := s.alert.HandleKey(ev)
s.alert.SetMessage(fmt.Sprintf("Alert Handled? %v -> %s", handled, ev.Name()))
return handled
}
=======
>>>>>>> 367d62ff009186e5aa584fd069dd57aaf7d46a8a
if ev.Key() == tcell.KeyF12 {
s.toggleCli()
return true
}
m := s.menuLayout.Menu()
if ev.Key() == tcell.KeyEscape {
return s.menuLayout.HandleKey(ev)
} else if m.Active() {
return s.menuLayout.HandleKey(ev)
}
if s.doOpen {
if ev.Key() == tcell.KeyCtrlU {
s.openPdsEntry.SetValue("")
return true
}
if ev.Key() == tcell.KeyEnter {
return s.cliGetPds("getpds", s.openPdsEntry.Value())
}
return s.openPdsEntry.HandleKey(ev)
}
return s.menuLayout.HandleKey(ev)
}
func (s *ScreenHome) HandleTime(ev *tcell.EventTime) {
s.menuLayout.HandleTime(ev)
s.loading.HandleTime(ev)
s.status.HandleTime(ev)
}
func (s *ScreenHome) Draw() {
if s.doOpen {
s.menuLayout.SetStyle(s.style.Foreground(tcell.ColorGray))
}
s.menuLayout.SetStyle(s.style)
m := s.menuLayout.Menu()
s.a.DrawWidget(s.menuLayout)
if s.doOpen && !m.Active() {
s.a.DrawWidget(s.openPdsEntry)
}
// These are outside of the menuLayout
if s.isLoading {
s.a.DrawWidget(s.loading)
}
s.a.DrawWidget(s.status)
}
func (s *ScreenHome) Exit() error { return nil }
func (s *ScreenHome) Log(t string, a ...any) {
s.cli.Log(t, a...)
s.showCli()
}
func (s *ScreenHome) initMenu() {
s.menuLayout.SetActive(true)
wrk := "[ ]"
if viper.GetBool(data.KeyVimMode) {
wrk = "[X]"
}
vimText := fmt.Sprintf("%s Vim Mode", wrk)
wrk = "[ ]"
if viper.GetBool(data.KeyRecNmInfer) {
wrk = "[X]"
}
inferRecNm := fmt.Sprintf("%s Infer Record Names", wrk)
s.menuLayout.AddMenuItems(
s.menuLayout.CreateMenuItem("file", "File", nil, 'f',
s.menuLayout.CreateMenuItem("file.openpds", "Open PDS", func() bool {
s.menuLayout.ToggleMenu()
s.doOpen = true
s.pdsListing.SetTitle(strings.Repeat(" ", 30))
return true
}, 'o'),
s.menuLayout.CreateMenuItem("file.reloadpds", "Reload PDS", func() bool {
s.menuLayout.ToggleMenu()
if s.activePds == nil {
return false
}
return s.cliGetPds("getpds", s.activePds.AtId.String())
}, 'r'),
s.menuLayout.CreateMenuItem("file.exit", "Exit", func() bool {
s.a.Exit()
return true
}, 'x'),
),
s.menuLayout.CreateMenuItem("settings", "Settings", nil, 's',
s.menuLayout.CreateMenuItem("settings.vimmode", vimText, func() bool {
s.menuLayout.ToggleMenu()
viper.Set(data.KeyVimMode, !viper.GetBool(data.KeyVimMode))
viper.WriteConfig()
s.update()
return true
}, 'v'),
s.menuLayout.CreateMenuItem("settings.inferrecnm", inferRecNm, func() bool {
s.menuLayout.ToggleMenu()
viper.Set(data.KeyRecNmInfer, !viper.GetBool(data.KeyRecNmInfer))
viper.WriteConfig()
s.update()
return true
}, 'r'),
),
)
}
func (s *ScreenHome) update() {
if s.doOpen {
s.menuLayout.SetWidget(s.openPdsEntry)
} else {
s.menuLayout.SetWidget(s.layout)
}
s.pdsListing.SetVimMode(viper.GetBool(data.KeyVimMode))
s.jsonContent.SetVimMode(viper.GetBool(data.KeyVimMode))
vimMI := s.menuLayout.FindItem("settings.vimmode")
wrk := "[ ]"
if viper.GetBool(data.KeyVimMode) {
wrk = "[X]"
}
vimMI.SetLabel(fmt.Sprintf("%s Vim Mode", wrk))
recNmInf := s.menuLayout.FindItem("settings.inferrecnm")
wrk = "[ ]"
if viper.GetBool(data.KeyRecNmInfer) {
wrk = "[X]"
}
recNmInf.SetLabel(fmt.Sprintf("%s Infer Record Names", wrk))
miReload := s.menuLayout.FindItem("file.reloadpds")
if s.activePds == nil {
miReload.SetDisabled(true)
miReload.SetStyle(s.style.Foreground(tcell.ColorGray))
} else {
miReload.SetDisabled(false)
miReload.SetStyle(s.style.Foreground(tcell.ColorLime))
}
}
func (s *ScreenHome) initCli() {
s.cli.SetVisible(true)
s.cli.AddCommand(w.NewCliCommand("getpds", s.cliGetPds))
s.cli.AddCommand(w.NewCliCommand("authpds", s.cliAuthPds))
s.cli.AddCommand(w.NewCliCommand("backuppds", s.cliBackupPds))
s.cli.AddCommand(w.NewCliCommand("sendstatus", s.cliSendStatus))
}
func (s *ScreenHome) toggleCli() {
if s.layout.Contains(s.cli) {
s.layout.Delete(s.cli)
s.layout.ActivateWidget(s.columns)
} else {
s.layout.Add(s.cli)
s.cli.SetVisible(true)
s.layout.ActivateWidget(s.cli)
}
}
func (s *ScreenHome) hideCli() {
s.cli.SetVisible(false)
if s.layout.Contains(s.cli) {
s.layout.Delete(s.cli)
}
}
func (s *ScreenHome) showCli() {
s.cli.SetVisible(true)
if !s.layout.Contains(s.cli) {
s.layout.Add(s.cli)
}
}
func (s *ScreenHome) cliGetPds(args ...string) bool {
s.isLoading = true
if len(args) < 1 {
s.Log("No id given.")
s.Log("Usage: 'getpds <atproto id>'")
s.isLoading = false
return true
}
go func() {
defer func() { s.isLoading = false }()
var pds *models.Pds
var err error
if s.activePds != nil && string(s.activePds.AtId) == args[1] {
pds, err = s.r.ReloadPds(args[1])
} else {
pds, err = s.r.GetPDS(args[1])
}
if err != nil {
s.Log(err.Error())
return
} else if pds == nil {
s.Log("PDS (%s) Not Found.", args[1])
return
}
s.doOpen = false
slog.Default().Debug(fmt.Sprintf("Retrieved: %s (%s) at %s", pds.AtId, pds.Did, time.Now().Format(time.RFC3339)))
s.activePds = pds
s.updatePdsListing()
n, err := s.pdsListing.GetActiveNode()
if err == nil && n != nil {
s.changePdsList(n)
}
s.layout.ActivateWidget(s.columns)
s.columns.ActivateWidget(s.pdsListing)
}()
return true
}
func (s *ScreenHome) cliAuthPds(args ...string) bool {
if s.activePds == nil {
s.Log("No active PDS.")
return true
}
s.isLoading = true
go func() {
defer func() { s.isLoading = false }()
atid := s.activePds.AtId.String()
callbackRes := make(chan url.Values, 1)
listenPort, err := s.r.Auth.ListenForCallback(callbackRes)
if err != nil {
s.Log("Error Instantiating HTTP Server for Callback: %w", err)
return
}
s.Log("Listening on %d", listenPort)
var authUrl string
authUrl, err = s.r.Auth.StartAuthFlow(listenPort, atid, callbackRes)
if err != nil {
s.Log("Error starting auth flow: %w", err)
}
<<<<<<< HEAD
s.alert.SetTitle("Authentication Started")
s.alert.SetMessage(fmt.Sprintf("OAuth Process Started.\nIf a browser window didn't open, you can open this URL manually:\n%s", authUrl))
s.alert.SetVisible(true)
s.alert.SetActive(true)
s.showAlert = true
=======
s.Log("Authentication Started")
s.Log(fmt.Sprintf("OAuth Process Started.\nIf a browser window didn't open, you can open this URL manually:\n%s", strings.ReplaceAll(authUrl, "%", "%%")))
>>>>>>> 367d62ff009186e5aa584fd069dd57aaf7d46a8a
}()
return true
}
func (s *ScreenHome) cliBackupPds(args ...string) bool {
if s.activePds == nil {
s.Log("No active PDS.")
return true
}
s.isLoading = true
go func() {
defer func() { s.isLoading = false }()
nm, sz, err := s.activePds.Backup()
if err != nil {
s.Log("Error: %w", err)
return
}
s.Log("Backup Created: %s (%d bytes)", nm, sz)
}()
return true
}
func (s *ScreenHome) cliSendStatus(args ...string) bool {
if s.activePds == nil {
s.Log("No active PDS.")
return true
}
s.isLoading = true
go func() {
defer func() { s.isLoading = false }()
if !s.r.Auth.HasAuth(s.activePds.Did) {
s.Log("Not authorized. Run `authpds`")
return
}
err := s.r.SendToPDS(s.activePds.Did)
if err != nil {
s.Log("Error sending status: %w", err)
}
}()
return true
}
func (s *ScreenHome) updatePdsListing() {
s.pdsListing.SetTitle(fmt.Sprintf("─ %s (%s)", s.activePds.AtId.String(), s.activePds.Did.String()))
s.pdsListing.Clear()
nsidList := s.activePds.NSIDStringList()
var tree []*wd.TreeNode
for i, v := range nsidList {
t := wd.NewTreeNode(v, v)
nsid := s.activePds.NSIDs[i]
rIds := s.activePds.GetRecordIdsFor(nsid)
for j := range rIds {
label := rIds[j]
if viper.GetBool(data.KeyRecNmInfer) {
if rec, ok := s.activePds.Records[rIds[j]]; ok {
for k := range rec {
if k == "name" || k == "title" || k == "label" || k == "displayName" {
if f, ok := rec[k].(string); ok {
label = fmt.Sprintf("%s (%s)", f, rIds[j])
break
}
}
}
}
}
c := wd.NewTreeNode(label, rIds[j])
t.AddChild(c)
}
tree = append(tree, t)
}
s.pdsListing.SetTree(tree)
s.layout.ActivateWidget(s.columns)
s.columns.ActivateWidget(s.pdsListing)
s.pdsListing.SetActive(true)
s.pdsListing.SetFocusable(true)
s.hideCli()
}
func (s *ScreenHome) selectPdsListingEntry(tn *wd.TreeNode) bool {
if !s.updateJsonView(tn) {
return false
}
if tn.HasChildren() {
tn.ToggleExpand()
s.pdsListing.UpdateList()
} else {
s.columns.ActivateWidget(s.jsonContent)
}
return true
}
func (s *ScreenHome) changePdsList(tn *wd.TreeNode) bool {
upd := s.updateJsonView(tn)
upd = s.updateStatusPathBlock(tn)
return upd
}
func (s *ScreenHome) updateStatusPathBlock(tn *wd.TreeNode) bool {
s.stPathBlock.SetParts(tn.GetLabelPath())
return true
}
func (s *ScreenHome) updateJsonView(tn *wd.TreeNode) bool {
// TODO: Update JSON View
if tn.Depth() == 0 {
nsid, err := syntax.ParseNSID(tn.Value())
if err != nil {
s.Log("error parsing NSID from %s: %w", tn.Value(), err)
return false
}
recordIds := s.activePds.GetRecordIdsFor(nsid)
s.jsonContent.SetValue(recordIds)
return true
} else {
s.jsonContent.SetValue(s.activePds.Records[tn.Value()])
}
return true
}

View File

@@ -41,6 +41,7 @@ var (
Name = "expds"
cfgFile string
ConfigDir = ""
debug = false
rootCmd = &cobra.Command{
Use: "expds",
Short: "Utility to edit and view PDS",
@@ -60,6 +61,7 @@ func Execute() {
func init() {
cobra.OnInitialize(initConfig)
rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file")
rootCmd.PersistentFlags().BoolVarP(&debug, "debug", "d", false, "debug mode")
}
func initConfig() {

View File

@@ -27,7 +27,7 @@ func (a *AppLogHandler) addGroups(groups ...string) { a.groups = append(a.group
// AppLogHandler can handle all levels
func (a *AppLogHandler) Enabled(_ context.Context, lvl slog.Level) bool { return lvl >= a.level }
func (a *AppLogHandler) Handle(ctx context.Context, rcd slog.Record) error {
func (a *AppLogHandler) Handle(_ context.Context, rcd slog.Record) error {
if a.logFunc == nil {
return errors.New("no log func defined")
}

View File

@@ -40,6 +40,7 @@ type Repo struct {
handler *AppLogHandler
logFunc func(string, ...any)
Logger *slog.Logger
context context.Context
}
@@ -56,6 +57,7 @@ func NewRepo() (*Repo, error) {
} else {
r.handler.SetLevel(slog.LevelWarn)
}
r.Logger = slog.Default()
var err error
r.Auth, err = NewAuthRepo(r)
if err != nil {
@@ -64,10 +66,7 @@ func NewRepo() (*Repo, error) {
return r, nil
}
func (r *Repo) GetPDS(atId string) (*models.Pds, error) {
if p, ok := r.LoadedPDSs[atId]; ok && time.Since(p.RefreshTime) < r.BestBy {
return p, nil
}
func (r *Repo) fetchPds(atId string) (*models.Pds, error) {
p, err := models.NewPdsFromDid(atId)
if err != nil {
return nil, err
@@ -76,8 +75,19 @@ func (r *Repo) GetPDS(atId string) (*models.Pds, error) {
return p, nil
}
func (r *Repo) SendToPDS() error {
session, err := r.Auth.GetSession()
func (r *Repo) ReloadPds(atId string) (*models.Pds, error) {
return r.fetchPds(atId)
}
func (r *Repo) GetPDS(atId string) (*models.Pds, error) {
if p, ok := r.LoadedPDSs[atId]; ok && time.Since(p.RefreshTime) < r.BestBy {
return p, nil
}
return r.fetchPds(atId)
}
func (r *Repo) SendToPDS(did syntax.DID) error {
session, err := r.Auth.GetSession(did)
if err != nil {
return err
}
@@ -94,11 +104,11 @@ func (r *Repo) SendToPDS() error {
var resp struct {
Uri syntax.ATURI `json:"uri"`
}
slog.Debug("posting expds status...")
r.Logger.Debug("posting expds status...")
if err := c.Post(r.context, "com.atproto.repo.CreateRecord", body, &resp); err != nil {
return err
}
slog.Debug(fmt.Sprintf("posted: %s :: %s", resp.Uri.Authority(), resp.Uri.RecordKey()))
r.Logger.Debug(fmt.Sprintf("posted: %s :: %s", resp.Uri.Authority(), resp.Uri.RecordKey()))
return nil
}
@@ -106,4 +116,6 @@ func (r *Repo) SetLogFunc(l func(string, ...any)) {
r.logFunc = l
r.handler = NewAppLogHandler(r.logFunc)
slog.SetDefault(slog.New(r.handler))
r.Logger = slog.Default()
r.Logger.Debug("New Log Func Set for slog")
}

View File

@@ -4,6 +4,7 @@ import (
"context"
"errors"
"fmt"
"log/slog"
"net"
"net/http"
"net/url"
@@ -13,6 +14,7 @@ import (
"time"
"github.com/bluesky-social/indigo/atproto/auth/oauth"
"github.com/bluesky-social/indigo/atproto/syntax"
"github.com/spf13/viper"
)
@@ -25,12 +27,15 @@ type AuthRepo struct {
session *oauth.ClientSessionData
authError error
Logger *slog.Logger
}
func NewAuthRepo(r *Repo) (*AuthRepo, error) {
a := &AuthRepo{
r: r,
context: r.context,
Logger: r.Logger,
}
var err error
a.oauthConfig, a.oauthClient, a.store, err = a.buildOAuthClient()
@@ -109,20 +114,22 @@ func (r *AuthRepo) ListenForCallback(res chan url.Values) (int, error) {
if !errors.Is(err, http.ErrServerClosed) {
panic(err)
}
r.Logger.Debug("Server Shut Down")
}()
return listener.Addr().(*net.TCPAddr).Port, nil
}
func (r *AuthRepo) HasAuth() bool {
sess, err := r.store.GetMostRecentSession(r.context)
return err != nil || sess == nil
func (r *AuthRepo) HasAuth(did syntax.DID) bool {
sess, err := r.GetSession(did)
return err == nil && sess != nil
}
func (r *AuthRepo) GetSession() (*oauth.ClientSession, error) {
sess, err := r.store.GetMostRecentSession(r.context)
func (r *AuthRepo) GetSession(did syntax.DID) (*oauth.ClientSession, error) {
sess, err := r.store.GetMostRecentSessionFor(r.context, did)
if err != nil {
return nil, fmt.Errorf("error getting most recent session: %w", err)
}
r.Logger.Warn(fmt.Sprintf("GetSession(): Resuming Session: %s (%s)", sess.SessionID, sess.AccountDID))
return r.oauthClient.ResumeSession(r.context, sess.AccountDID, sess.SessionID)
}

139
data/repo_auth.go.orig Normal file
View File

@@ -0,0 +1,139 @@
package data
import (
"context"
"errors"
"fmt"
"log/slog"
"net"
"net/http"
"net/url"
"os/exec"
"path/filepath"
"strings"
"time"
"github.com/bluesky-social/indigo/atproto/auth/oauth"
"github.com/bluesky-social/indigo/atproto/syntax"
"github.com/spf13/viper"
)
type AuthRepo struct {
r *Repo
oauthClient *oauth.ClientApp
oauthConfig *oauth.ClientConfig
store *SqliteStore
context context.Context
<<<<<<< HEAD
session *oauth.ClientSessionData
authError error
=======
session *oauth.ClientSessionData
Logger *slog.Logger
>>>>>>> 367d62ff009186e5aa584fd069dd57aaf7d46a8a
}
func NewAuthRepo(r *Repo) (*AuthRepo, error) {
a := &AuthRepo{
r: r,
context: r.context,
Logger: r.Logger,
}
var err error
a.oauthConfig, a.oauthClient, a.store, err = a.buildOAuthClient()
if err != nil {
return nil, err
}
return a, nil
}
// Build the OAuthClient connected to our sqlite db
func (r *AuthRepo) buildOAuthClient() (*oauth.ClientConfig, *oauth.ClientApp, *SqliteStore, error) {
config := oauth.ClientConfig{
ClientID: "https://expds.bullercodeworks.com/oauth-client-metadata.json",
Scopes: []string{"atproto", "repo:*", "blob:*/*"},
UserAgent: "expds",
}
store, err := NewSqliteStore(&SqliteStoreConfig{
DatabasePath: r.prepareDbPath(),
SessionExpiryDuration: time.Hour * 24 * 90,
SessionInactivityDuration: time.Hour * 24 * 14,
AuthRequestExpiryDuration: time.Minute * 30,
})
if err != nil {
return nil, nil, nil, err
}
oauthClient := oauth.NewClientApp(&config, store)
return &config, oauthClient, store, nil
}
func (r *AuthRepo) StartAuthFlow(port int, identifier string, callbackRes chan url.Values) (string, error) {
r.oauthConfig.CallbackURL = fmt.Sprintf("http://127.0.0.1:%d/callback", port)
authUrl, err := r.oauthClient.StartAuthFlow(r.context, identifier)
if err != nil {
return "", fmt.Errorf("error logging in: %w", err)
}
if !strings.HasPrefix(authUrl, "https://") {
return "", fmt.Errorf("non-https authUrl")
}
go func() {
exec.Command("xdg-open", authUrl).Start()
r.session, r.authError = r.oauthClient.ProcessCallback(r.context, <-callbackRes)
}()
return authUrl, nil
}
// Follows XDG conventions and creates the directories if necessary.
// By default, on linux, this will be "~/.local/share/go-oauth-cli-app/oauth_sessions.sqlite3"
func (r *AuthRepo) prepareDbPath() string {
return filepath.Join(viper.GetString(KeyDataDir), "expds.sqlite3")
}
// HTTP Server listening for OAuth Response
func (r *AuthRepo) ListenForCallback(res chan url.Values) (int, error) {
listener, err := net.Listen("tcp", ":0")
if err != nil {
return 0, err
}
mux := http.NewServeMux()
server := &http.Server{
Handler: mux,
}
mux.HandleFunc("/callback", func(w http.ResponseWriter, req *http.Request) {
res <- req.URL.Query()
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.WriteHeader(200)
w.Write([]byte("<!DOCTYPE html><html><body><h2>expds</h2><p>You can safely close this window and return to your application.</p></body></html>\n"))
go server.Shutdown(r.context)
})
go func() {
err := server.Serve(listener)
if !errors.Is(err, http.ErrServerClosed) {
panic(err)
}
r.Logger.Debug("Server Shut Down")
}()
return listener.Addr().(*net.TCPAddr).Port, nil
}
func (r *AuthRepo) HasAuth(did syntax.DID) bool {
sess, err := r.GetSession(did)
return err == nil && sess != nil
}
func (r *AuthRepo) GetSession(did syntax.DID) (*oauth.ClientSession, error) {
sess, err := r.store.GetMostRecentSessionFor(r.context, did)
if err != nil {
return nil, fmt.Errorf("error getting most recent session: %w", err)
}
r.Logger.Warn(fmt.Sprintf("GetSession(): Resuming Session: %s (%s)", sess.SessionID, sess.AccountDID))
return r.oauthClient.ResumeSession(r.context, sess.AccountDID, sess.SessionID)
}

View File

@@ -96,6 +96,20 @@ func (m *SqliteStore) GetSession(ctx context.Context, did syntax.DID, sessionID
return &row.Data, nil
}
func (m *SqliteStore) GetMostRecentSessionFor(ctx context.Context, did syntax.DID) (*oauth.ClientSessionData, error) {
var row storedSessionData
res := m.db.WithContext(ctx).Where(&storedSessionData{
AccountDid: did,
}).Order(clause.OrderByColumn{
Column: clause.Column{Name: "updated_at"},
Desc: true,
}).First(&row)
if res.Error != nil {
return nil, res.Error
}
return &row.Data, nil
}
// not part of the ClientAuthStore interface, just used for the CLI app
func (m *SqliteStore) GetMostRecentSession(ctx context.Context) (*oauth.ClientSessionData, error) {
var row storedSessionData

View File

@@ -4,6 +4,7 @@ import (
"errors"
"fmt"
"strings"
"sync"
h "git.bullercodeworks.com/brian/expds/helpers"
t "git.bullercodeworks.com/brian/tcell-widgets"
@@ -36,7 +37,12 @@ type TreeBrowser struct {
keyMap *t.KeyMap
vimMode bool
searching bool
searchStr string
logger func(string, ...any)
m sync.Mutex
}
var _ t.Widget = (*TreeBrowser)(nil)
@@ -55,6 +61,10 @@ func (w *TreeBrowser) Init(id string, style tcell.Style) {
t.NewKey(t.BuildEK(tcell.KeyUp), func(_ *tcell.EventKey) bool { return w.MoveUp() }),
t.NewKey(t.BuildEK(tcell.KeyDown), func(_ *tcell.EventKey) bool { return w.MoveDown() }),
t.NewKey(t.BuildEK(tcell.KeyEnter), func(ev *tcell.EventKey) bool {
if w.searching {
w.searching = !w.searching
return true
}
if w.onSelect != nil {
n, err := w.GetActiveNode()
if err != nil || n == nil {
@@ -97,6 +107,14 @@ func (w *TreeBrowser) Init(id string, style tcell.Style) {
}
return false
}),
t.NewKey(t.BuildEKr('/'), func(ev *tcell.EventKey) bool {
if !w.searching {
w.searching = true
w.searchStr = ""
return true
}
return false
}),
)
}
@@ -111,12 +129,20 @@ func (w *TreeBrowser) HandleKey(ev *tcell.EventKey) bool {
if !w.active || !w.focusable {
return false
}
return w.keyMap.Handle(ev)
if w.keyMap.Handle(ev) {
return true
} else if w.searching {
w.updateSearch(ev)
return true
}
return false
}
func (w *TreeBrowser) HandleTime(ev *tcell.EventTime) {}
func (w *TreeBrowser) Draw(screen tcell.Screen) {
w.m.Lock()
defer w.m.Unlock()
if !w.visible {
return
}
@@ -170,9 +196,23 @@ func (w *TreeBrowser) Draw(screen tcell.Screen) {
if len(txt) > w.w-brdSz && w.w-brdSz >= 0 {
txt = txt[:(w.w - brdSz)]
}
if w.searching && strings.Contains(txt, w.searchStr) {
// TODO: Fix multi-byte depth indicator
srchIdx := strings.Index(txt, w.searchStr)
endSrchIdx := srchIdx + len(w.searchStr)
wh.DrawText(x, y, txt[:srchIdx], dS.Reverse(rev), screen)
wh.DrawText(x+srchIdx, y, txt[srchIdx:endSrchIdx], dS.Reverse(!rev), screen)
if len(txt) > endSrchIdx {
wh.DrawText(x+endSrchIdx, y, txt[endSrchIdx:], dS.Reverse(rev), screen)
}
} else {
wh.DrawText(x, y, txt, dS.Reverse(rev), screen)
}
y += 1
}
if w.searching {
wh.DrawText(w.x, w.y+w.h, fmt.Sprintf("Searching: %s", w.searchStr), dS, screen)
}
}
func (w *TreeBrowser) SetStyle(s tcell.Style) { w.style = s }
@@ -197,6 +237,8 @@ func (w *TreeBrowser) SetW(wd int) { w.SetSize(t.Coord{X: wd, Y: w.h}) }
func (w *TreeBrowser) SetH(h int) { w.SetSize(t.Coord{X: w.w, Y: h}) }
func (w *TreeBrowser) SetSize(c t.Coord) { w.w, w.h = c.X, c.Y }
func (w *TreeBrowser) WantW() int {
w.m.Lock()
defer w.m.Unlock()
var want int
for i := range w.list {
want = h.MaxI(want, len(w.list[i]))
@@ -204,6 +246,8 @@ func (w *TreeBrowser) WantW() int {
return w.w
}
func (w *TreeBrowser) WantH() int {
w.m.Lock()
defer w.m.Unlock()
want := len(w.list)
if len(w.border) > 0 {
return want + 2
@@ -225,8 +269,8 @@ func (w *TreeBrowser) ClearBorder() { w.border = []rune{}
func (w *TreeBrowser) SetOnChange(c func(*TreeNode) bool) { w.onChange = c }
func (w *TreeBrowser) SetOnSelect(s func(*TreeNode) bool) { w.onSelect = s }
func (w *TreeBrowser) SetVimMode(b bool) { w.vimMode = b }
func (w *TreeBrowser) GetActiveNode() (*TreeNode, error) {
if len(w.listNodes) < 0 {
func (w *TreeBrowser) nmGetActiveNode() (*TreeNode, error) {
if len(w.listNodes) <= 0 {
return nil, errors.New("no nodes")
}
if w.cursor < 0 {
@@ -237,6 +281,11 @@ func (w *TreeBrowser) GetActiveNode() (*TreeNode, error) {
}
return w.listNodes[w.cursor], nil
}
func (w *TreeBrowser) GetActiveNode() (*TreeNode, error) {
w.m.Lock()
defer w.m.Unlock()
return w.nmGetActiveNode()
}
func (w *TreeBrowser) SetCursorWrap(b bool) { w.cursorWrap = b }
func (w *TreeBrowser) MoveUp() bool {
if w.cursor > 0 {
@@ -305,22 +354,30 @@ func (w *TreeBrowser) PageDn() bool {
func (w *TreeBrowser) Title() string { return w.title }
func (w *TreeBrowser) SetTitle(ttl string) { w.title = ttl }
func (w *TreeBrowser) SetTree(l []*TreeNode) {
w.m.Lock()
defer w.m.Unlock()
w.nodes = l
w.UpdateList()
w.nmUpdateList()
}
func (w *TreeBrowser) Clear() {
w.m.Lock()
defer w.m.Unlock()
w.nodes = []*TreeNode{}
w.UpdateList()
w.nmUpdateList()
}
func (w *TreeBrowser) Add(n *TreeNode) {
w.m.Lock()
defer w.m.Lock()
if n.depthIndic == "" {
n.depthIndic = w.depthIndic
}
w.nodes = append(w.nodes, n)
w.UpdateList()
w.nmUpdateList()
}
func (w *TreeBrowser) UpdateList() {
// Update the list, intended to be called locally within other functions that
// handle the mutex
func (w *TreeBrowser) nmUpdateList() {
w.list = []string{}
w.listNodes = []*TreeNode{}
for i := range w.nodes {
@@ -335,6 +392,98 @@ func (w *TreeBrowser) UpdateList() {
}
}
func (w *TreeBrowser) UpdateList() {
w.m.Lock()
defer w.m.Unlock()
w.nmUpdateList()
}
func (w *TreeBrowser) nmSetNodeActive(tn *TreeNode) {
// Make sure that the selected node is visible
wrk := tn.parent
for wrk != nil {
wrk.expanded = true
wrk = wrk.parent
}
w.nmUpdateList()
for i := range w.listNodes {
if w.listNodes[i] == tn {
w.cursor = i
return
}
}
}
func (w *TreeBrowser) SetNodeActive(tn *TreeNode) {
w.m.Lock()
defer w.m.Unlock()
w.nmSetNodeActive(tn)
}
func (w *TreeBrowser) updateSearch(ev *tcell.EventKey) {
w.m.Lock()
defer w.m.Unlock()
if len(w.nodes) == 0 {
return
}
if wh.IsBS(*ev) {
if len(w.searchStr) > 0 {
w.searchStr = w.searchStr[:len(w.searchStr)-1]
if len(w.searchStr) == 0 {
w.searching = false
}
}
return
}
w.searchStr = fmt.Sprintf("%s%s", w.searchStr, string(ev.Rune()))
wrk, _ := w.nmGetActiveNode()
if wrk == nil {
wrk = w.nodes[0]
}
// Check the ative node & it's children for the search
if fnd := wrk.SearchLabels(w.searchStr); fnd != nil {
w.nmSetNodeActive(fnd)
return
}
// Didn't find a child of the active node that matched, look for a sibling
if wrk.parent != nil {
if fnd := wrk.parent.SearchLabels(w.searchStr); fnd != nil {
w.nmSetNodeActive(fnd)
return
}
}
// Check the next browser node
var stIdx int
for i := range w.nodes {
if w.nodes[i] == wrk {
stIdx = i + 1
break
}
}
for i := 0; i < len(w.nodes); i++ {
idx := (i + stIdx) % len(w.nodes)
if fnd := w.nodes[idx].SearchLabels(w.searchStr); fnd != nil {
w.nmSetNodeActive(fnd)
return
}
}
}
func (w *TreeBrowser) getCurrentLine() string {
w.m.Lock()
defer w.m.Unlock()
l := len(w.list)
if l == 0 {
return ""
}
if w.cursor < 0 || w.cursor >= l {
return ""
}
return w.list[w.cursor]
}
/*
* Tree Node
*/
@@ -370,6 +519,7 @@ func (tn *TreeNode) GetLabelPath() []string {
}
return append(path, tn.Label())
}
func (tn *TreeNode) getList() []string {
pre := strings.Repeat(tn.depthIndic, tn.Depth())
ret := []string{fmt.Sprintf("%s%s", pre, tn.label)}
@@ -380,6 +530,7 @@ func (tn *TreeNode) getList() []string {
}
return ret
}
func (tn *TreeNode) getVisibleNodeList() []*TreeNode {
ret := []*TreeNode{tn}
if tn.expanded {
@@ -390,6 +541,19 @@ func (tn *TreeNode) getVisibleNodeList() []*TreeNode {
return ret
}
func (tn *TreeNode) SearchLabels(f string) *TreeNode {
if strings.Contains(tn.label, f) {
return tn
}
for i := 0; i < len(tn.children); i++ {
fnd := tn.children[i].SearchLabels(f)
if fnd != nil {
return fnd
}
}
return nil
}
func (tn *TreeNode) ToggleExpand() { tn.expanded = !tn.expanded }
func (tn *TreeNode) AddChild(t *TreeNode, rest ...*TreeNode) {
@@ -415,3 +579,44 @@ func (tn *TreeNode) GetPath() []string {
}
return append(path, tn.value)
}
func (tn *TreeNode) GetChildren() []*TreeNode { return tn.children }
func (tn *TreeNode) GetFirstChild() *TreeNode {
if !tn.HasChildren() {
return nil
}
return tn.children[0]
}
func (tn *TreeNode) GetPrevChild(before *TreeNode) *TreeNode {
if !tn.HasChildren() {
return nil
}
var found bool
for i := len(tn.children) - 1; i >= 0; i-- {
if found {
return tn.children[i]
}
if tn.children[i] == before {
found = true
}
}
return nil
}
func (tn *TreeNode) GetNextChild(after *TreeNode) *TreeNode {
if !tn.HasChildren() {
return nil
}
var found bool
for i := range tn.children {
if found {
return tn.children[i]
}
if tn.children[i] == after {
found = true
}
}
return nil
}