Skip to content

Commit

Permalink
add init command, better sign in prompt
Browse files Browse the repository at this point in the history
  • Loading branch information
treethought committed Jun 10, 2024
1 parent 64aa4b3 commit a7b1d0c
Show file tree
Hide file tree
Showing 12 changed files with 173 additions and 115 deletions.
60 changes: 32 additions & 28 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,39 +9,16 @@ application, or as a hosted SSH app using

![tofui screenshot](./media/screenshot.png)

## Hosted version
## Running Locally

Use a hosted instance of tofui over ssh

```
ssh -p 42069 tofui.xyz
Running locally requires your own Neynar application. After creating one, run the following to create your config file

(Note this is WIP and may be slow / unavailable)
```
tofui init
```

### SSH Sessions, Authentication and Details

Each SSH session is authenticated via it's SSH public key. The session then
receives it's own [Bubble Tea](https://github.com/charmbracelet/bubbletea) which
provides the interface.

For authorization, the app directs you to create a signer via Neynar's
[SIWN](https://docs.neynar.com/docs/how-to-let-users-connect-farcaster-accounts-with-write-access-for-free-using-sign-in-with-neynar-siwn).
This signer is created and managed by Neynar, and is used to provide tofui
access to your farcaster account via it's API.

This is done when both running locally and over SSH, and the signer is specific
to whichever app credentials were used. This would be tofui over SSH, or your
own app when running locally.

the tofui instance (local or hosted) uses the configured Neynar app credentials
to obtain a signer via

## Running Locally

Running locally requires your own Neynar application. After creating one, copy
[config.yaml.example](./config.yaml.example) to config.yaml and updating with
your app's values.
Starting tofui the first time will then give you the option to sign in

### Install

Expand Down Expand Up @@ -89,3 +66,30 @@ Then start the TUI via `tofui`
| C | Open reply form when viewing cast |
| o | Open current cast in browser (local mode only) |
| l | Like current cast |

## Hosted version (WIP and often unavailable)

Use a hosted instance of tofui over ssh. (Note: this is WIP and currently unavailable)

```
ssh -p 42069 tofui.xyz
```

### SSH Sessions, Authentication and Details

Each SSH session is authenticated via it's SSH public key. The session then
receives it's own [Bubble Tea](https://github.com/charmbracelet/bubbletea) which
provides the interface.

For authorization, the app directs you to create a signer via Neynar's
[SIWN](https://docs.neynar.com/docs/how-to-let-users-connect-farcaster-accounts-with-write-access-for-free-using-sign-in-with-neynar-siwn).
This signer is created and managed by Neynar, and is used to provide tofui
access to your farcaster account via it's API.

This is done when both running locally and over SSH, and the signer is specific
to whichever app credentials were used. This would be tofui over SSH, or your
own app when running locally.

the tofui instance (local or hosted) uses the configured Neynar app credentials
to obtain a signer via

2 changes: 1 addition & 1 deletion api/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ func NewClient(cfg *config.Config) *Client {
client = &Client{
c: http.DefaultClient,
apiKey: cfg.Neynar.APIKey,
baseURL: cfg.Neynar.HubURL,
baseURL: cfg.Neynar.BaseUrl,
clientID: cfg.Neynar.ClientID,
}
})
Expand Down
67 changes: 67 additions & 0 deletions cmd/init.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package cmd

import (
"bufio"
"fmt"
"log"
"os"
"path/filepath"

"github.com/spf13/cobra"
"gopkg.in/yaml.v3"

"github.com/treethought/tofui/config"
)

var initCmd = &cobra.Command{
Use: "init",
Short: "init tofui config",
Run: func(cmd *cobra.Command, args []string) {
home, err := os.UserHomeDir()
if err != nil {
log.Fatal("failed to get user home directory")
}
fmt.Println("To use tofui locally, you will need to create a Neynar app")
reader := bufio.NewReader(os.Stdin)

fmt.Print("Enter Client ID (found at https://dev.neynar.com/app): \n")
clientID, _ := reader.ReadString('\n')
clientID = clientID[:len(clientID)-1] // Trim newline character

fmt.Print("Enter API Key (found at https://dev.neynar.com/): \n")
apiKey, _ := reader.ReadString('\n')
apiKey = apiKey[:len(apiKey)-1] // Trim newline character

cfg := &config.Config{}
cfg.Neynar.ClientID = clientID
cfg.Neynar.APIKey = apiKey
cfg.Neynar.BaseUrl = "https://api.neynar.com/v2/farcaster"
cfg.Server.Host = "localhost"
cfg.Server.HTTPPort = 4200
cfg.DB.Dir = filepath.Join(home, ".tofui", "db")
cfg.Log.Path = filepath.Join(home, ".tofui", "debug.log")

path := filepath.Join(home, ".tofui", "config.yaml")

data, err := yaml.Marshal(cfg)
if err != nil {
log.Fatalf("error: %v", err)
}
err = os.MkdirAll(filepath.Dir(path), 0755)
if err != nil {
log.Fatalf("failed to create config directory: %v", err)
}
if err = os.WriteFile(path, data, 0644); err != nil {
log.Fatalf("error: %v", err)
}
fmt.Printf("Wrote config file created at %s\n", path)
fmt.Println("Note: You must add 'http://localhost:4200' to your Neynar app's Authorzed origins to sign in!!")
fmt.Println("\nYou can now run `tofui` to start the app")

},
}

func init() {
rootCmd.AddCommand(initCmd)

}
14 changes: 12 additions & 2 deletions cmd/root.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package cmd

import (
"errors"
"fmt"
"log"
"os"
Expand Down Expand Up @@ -58,17 +59,26 @@ func init() {
}

func initConfig() {
os.MkdirAll("/tmp/tofui", 0755)
if len(os.Args) > 1 && os.Args[1] == "init" {
return
}
var err error
if configPath == "" {
if _, err := os.Stat("config.yaml"); err == nil {
configPath = "config.yaml"
} else {
configPath = "tofui.yaml"
homeDir, err := os.UserHomeDir()
if err != nil {
log.Fatal("failed to find default config file: ", err)
}
configPath = filepath.Join(homeDir, ".tofui", "config.yaml")
}
}
cfg, err = config.ReadConfig(configPath)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
log.Fatal("failed to fing config file, run `tofui init` to create one")
}
log.Fatal("failed to read config: ", err)
}

Expand Down
9 changes: 8 additions & 1 deletion config.yaml.example
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
neynar:
api_key: "1234"
client_id: "abcd"
hub_url: "https://api.neynar.com/v2/farcaster"
base_url: "https://api.neynar.com/v2/farcaster"
db:
dir: .db
log:
path: debug.log
server:
host: localhost
http_port: 4200
2 changes: 1 addition & 1 deletion config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ type Config struct {
Neynar struct {
APIKey string `yaml:"api_key"`
ClientID string `yaml:"client_id"`
HubURL string `yaml:"hub_url"`
BaseUrl string `yaml:"base_url"`
}
}

Expand Down
2 changes: 1 addition & 1 deletion db/db.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ func (db *DB) runGC() {

func (db *DB) Close() {
slog.Println("closing db")
if db.db != nil {
if db != nil && db.db != nil {
db.db.Close()
db.lf.Close()
}
Expand Down
37 changes: 13 additions & 24 deletions ui/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,9 +70,9 @@ type App struct {
showQuickSelect bool
publish *PublishInput
statusLine *StatusLine
signinPrompt *SigninPrompt
splash *SplashView
help *HelpView
// signinPrompt *SigninPrompt
splash *SplashView
help *HelpView
}

func (a *App) PublicKey() string {
Expand Down Expand Up @@ -128,9 +128,11 @@ func NewApp(cfg *config.Config, ctx *AppContext) *App {
a.publish = NewPublishInput(a)
a.statusLine = NewStatusLine(a)
a.help = NewHelpView(a)
a.signinPrompt = NewSigninPrompt()
a.splash = NewSplashView()
a.splash = NewSplashView(a)
a.splash.SetActive(true)
if a.ctx.signer == nil {
a.splash.ShowSignin(true)
}
a.SetNavName("feed")

feed := NewFeedView(a)
Expand Down Expand Up @@ -170,10 +172,6 @@ func (a *App) SetFocus(name string) tea.Cmd {
a.publish.SetActive(false)
a.publish.SetFocus(false)
}
if a.signinPrompt.Active() {
a.signinPrompt.SetActive(false)
a.signinPrompt.SetContent("")
}
if name == "" || name == a.focused {
return nil
}
Expand Down Expand Up @@ -242,7 +240,7 @@ func (a *App) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case *UpdateSignerMsg:
a.ctx.signer = msg.Signer
a.signinPrompt.SetActive(false)
a.splash.ShowSignin(false)
log.Println("updated signer for: ", msg.Signer.Username)
return a, a.Init()
case navNameMsg:
Expand Down Expand Up @@ -320,7 +318,7 @@ func (a *App) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
pw := wx - sx
py := wy - 10
a.publish.SetSize(pw, py)
a.signinPrompt.SetSize(pw, py)
a.splash.SetSize(pw, py)

hw := wx - sx
hy := wy - 10
Expand All @@ -343,8 +341,10 @@ func (a *App) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
case "ctrl+c", "q":
return a, tea.Quit
}

if a.splash.Active() {
return a, nil
_, cmd := a.splash.Update(msg)
return a, cmd
}
if a.publish.Active() {
_, cmd := a.publish.Update(msg)
Expand All @@ -358,14 +358,7 @@ func (a *App) Update(msg tea.Msg) (tea.Model, tea.Cmd) {

case *currentAccountMsg:
_, cmd := a.sidebar.Update(msg)
return a, cmd
}
if a.splash.Active() {
_, cmd := a.splash.Update(msg)
return a, cmd
}
if a.signinPrompt.Active() {
_, cmd := a.signinPrompt.Update(msg)
a.splash.ShowSignin(false)
return a, cmd
}
if a.publish.Active() {
Expand Down Expand Up @@ -413,10 +406,6 @@ func (a *App) View() string {
main = a.splash.View()
}

if a.signinPrompt.Active() {
main = a.signinPrompt.View()
}

if a.publish.Active() {
main = a.publish.View()
}
Expand Down
3 changes: 3 additions & 0 deletions ui/feed.go
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,9 @@ func likeCastCmd(client *api.Client, signer *api.Signer, cast *api.Cast) tea.Cmd

func getDefaultFeedCmd(client *api.Client, signer *api.Signer) tea.Cmd {
req := &api.FeedRequest{Limit: 100}
if signer == nil {
return nil
}
if signer != nil {
req.FID = signer.FID
req.ViewerFID = signer.FID
Expand Down
13 changes: 0 additions & 13 deletions ui/sidebar.go
Original file line number Diff line number Diff line change
Expand Up @@ -101,8 +101,6 @@ func (m *Sidebar) navHeader() []list.Item {
items := []list.Item{}
if api.GetSigner(m.app.ctx.pk) != nil {
items = append(items, &sidebarItem{name: "profile"})
} else {
items = append(items, &sidebarItem{name: "sign in"})
}
items = append(items, &sidebarItem{name: "feed"})
items = append(items, &sidebarItem{name: "--channels---", value: "--channels--", icon: "🏠"})
Expand Down Expand Up @@ -144,17 +142,6 @@ func (m *Sidebar) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
}
if msg.String() == "enter" {
currentItem := m.nav.SelectedItem().(*sidebarItem)
if currentItem.name == "sign in" {
log.Println("sign in selected")
portPart := fmt.Sprintf(":%d", m.app.cfg.Server.HTTPPort)
if portPart == ":443" {
portPart = ""
}
u := fmt.Sprintf("%s/signin?pk=%s", m.app.cfg.BaseURL(), m.app.ctx.pk)
m.app.signinPrompt.SetContent(fmt.Sprintf("Please sign in at %s", u))
m.app.signinPrompt.SetActive(true)
return m, OpenURL(u)
}
if currentItem.name == "profile" {
m.SetActive(false)
log.Println("profile selected")
Expand Down
41 changes: 0 additions & 41 deletions ui/signin_prompt.go

This file was deleted.

Loading

0 comments on commit a7b1d0c

Please sign in to comment.