6
1
mirror of https://git.mills.io/saltyim/saltyim.git synced 2024-06-16 03:48:24 +00:00

Refactor Endpoint for automatic endpoint discovery and less configuration (#46)

Co-authored-by: James Mills <prologic@shortcircuit.net.au>
Reviewed-on: https://git.mills.io/prologic/saltyim/pulls/46
This commit is contained in:
James Mills 2022-03-23 12:39:31 +00:00
parent 03a99bdccf
commit 7ffefb042e
16 changed files with 165 additions and 160 deletions

@ -3,10 +3,8 @@ package saltyim
import ( import (
"bytes" "bytes"
"context" "context"
"encoding/json"
"fmt" "fmt"
"net/http" "net/http"
"net/url"
"os" "os"
"time" "time"
@ -23,7 +21,7 @@ type configCache map[string]Config
// PackMessage formts an outoing message in the Message Format // PackMessage formts an outoing message in the Message Format
// <timestamp>\t(<sender>) <message> // <timestamp>\t(<sender>) <message>
func PackMessage(me Addr, msg string) []byte { func PackMessage(me *Addr, msg string) []byte {
return []byte(fmt.Sprint(time.Now().UTC().Format(time.RFC3339), "\t", me.Formatted(), "\t", msg)) return []byte(fmt.Sprint(time.Now().UTC().Format(time.RFC3339), "\t", me.Formatted(), "\t", msg))
} }
@ -41,40 +39,38 @@ func Send(endpoint, msg string) error {
// Client is a Salty IM client that handles talking to a Salty IM Broker // Client is a Salty IM client that handles talking to a Salty IM Broker
// and Sedngina and Receiving messages to/from Salty IM Users. // and Sedngina and Receiving messages to/from Salty IM Users.
type Client struct { type Client struct {
key *keys.EdX25519Key me *Addr
key *keys.EdX25519Key
endpoint string
me Addr
cache configCache cache configCache
} }
// NewClient reeturns a new Salty IM client for sending and receiving // NewClient reeturns a new Salty IM client for sending and receiving
// encrypted messages to other Salty IM users as well as decrypting // encrypted messages to other Salty IM users as well as decrypting
// and displaying messages of the user's own inbox. // and displaying messages of the user's own inbox.
func NewClient(me Addr, identity, endpoint string) (*Client, error) { func NewClient(me *Addr, identity string) (*Client, error) {
key, m, err := GetIdentity(identity) key, m, err := GetIdentity(identity)
if err != nil { if err != nil {
return nil, fmt.Errorf("error opening identity %s: %w", identity, err) return nil, fmt.Errorf("error opening identity %s: %w", identity, err)
} }
if me.IsZero() { if me == nil || me.IsZero() {
me = m me = m
} }
if me.IsZero() { if me == nil || me.IsZero() {
return nil, fmt.Errorf("unable to find your user addressn in %s", identity) return nil, fmt.Errorf("unable to find your user addressn in %s", identity)
} }
if err := me.Refresh(); err != nil {
return nil, fmt.Errorf("error looking up user endpoint: %w", err)
}
log.Debugf("Using identity %s with public key %s", identity, key) log.Debugf("Using identity %s with public key %s", identity, key)
log.Debugf("Salty Addr is %s", me) log.Debugf("Salty Addr is %s", me)
log.Debugf("Endpoint is %s", endpoint) log.Debugf("Endpoint is %s", me.Endpoint())
return &Client{ return &Client{
key: key, me: me,
key: key,
endpoint: endpoint,
me: me,
cache: make(configCache), cache: make(configCache),
}, nil }, nil
} }
@ -125,16 +121,12 @@ func (cli *Client) handleMessage(prehook, posthook string, msgs chan string) msg
} }
} }
func (cli *Client) Me() Addr { return cli.me } func (cli *Client) Me() *Addr { return cli.me }
func (cli *Client) Endpoint() string { return cli.endpoint } func (cli *Client) Key() *keys.EdX25519PublicKey { return cli.key.PublicKey() }
// Read subscribers to this user's inbox for new messages // Read subscribers to this user's inbox for new messages
func (cli *Client) Read(ctx context.Context, endpoint, prehook, posthook string) chan string { func (cli *Client) Read(ctx context.Context, prehook, posthook string) chan string {
if endpoint == "" { uri, inbox := SplitInbox(cli.me.Endpoint().String())
endpoint = cli.endpoint
}
uri, inbox := SplitInbox(endpoint)
bus := msgbus_client.NewClient(uri, nil) bus := msgbus_client.NewClient(uri, nil)
msgs := make(chan string) msgs := make(chan string)
@ -173,31 +165,5 @@ func (cli *Client) Send(user, msg string) error {
// Register sends a registration requestn to a broker // Register sends a registration requestn to a broker
func (cli *Client) Register() error { func (cli *Client) Register() error {
req := RegisterRequest{
Addr: cli.me,
Key: cli.key.ID().String(),
}
data, err := json.Marshal(req)
if err != nil {
return fmt.Errorf("error serializing register request: %w", err)
}
signed, err := salty.Sign(cli.key, data)
if err != nil {
return fmt.Errorf("error signing registration request: %w", err)
}
body := bytes.NewBuffer(signed)
endpointURL, err := url.Parse(cli.endpoint)
if err != nil {
return fmt.Errorf("error parsing endpoint %s: %w", cli.endpoint, err)
}
endpointURL.Path = "/api/v1/register"
res, err := Request(http.MethodPost, endpointURL.String(), nil, body)
if err != nil {
return fmt.Errorf("error registering to broker %s: %w", endpointURL, err)
}
defer res.Body.Close()
return nil return nil
} }

@ -15,31 +15,30 @@ import (
var chatCmd = &cobra.Command{ var chatCmd = &cobra.Command{
Use: "chat <user>", Use: "chat <user>",
Short: "Creates a chat with a specific user", Short: "Creates a chat with a specific user",
Long: `This command creates a chat with the specified user by subscribing Long: `This command creates a chat with the specified user by discovering
to your default inbox (normally $USER) and prompts for input and sends encrypted and subscribing to your endpoint and prompts for input and sends encrypted
messages to the user via their endpoint.`, messages to the user via their discovered endpoint.`,
Args: cobra.MinimumNArgs(1), Args: cobra.MinimumNArgs(1),
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
user := viper.GetString("user") user := viper.GetString("user")
endpoint := viper.GetString("endpoint")
identity := viper.GetString("identity") identity := viper.GetString("identity")
var profiles []profile var profiles []profile
viper.UnmarshalKey("profiles", &profiles) viper.UnmarshalKey("profiles", &profiles)
for _, p := range profiles { for _, p := range profiles {
if user == p.User { if user == p.User {
endpoint = p.Endpoint
identity = p.Identity identity = p.Identity
} }
} }
var me saltyim.Addr me := &saltyim.Addr{}
if sp := strings.Split(user, "@"); len(sp) > 1 { if sp := strings.Split(user, "@"); len(sp) > 1 {
me.User = sp[0] me.User = sp[0]
me.Domain = sp[1] me.Domain = sp[1]
} }
// XXX: What if me.IsZero()
chat(me, identity, endpoint, args[0]) chat(me, identity, args[0])
}, },
} }
@ -47,8 +46,8 @@ func init() {
rootCmd.AddCommand(chatCmd) rootCmd.AddCommand(chatCmd)
} }
func chat(me saltyim.Addr, identity, endpoint, user string) { func chat(me *saltyim.Addr, identity, user string) {
cli, err := saltyim.NewClient(me, identity, endpoint) cli, err := saltyim.NewClient(me, identity)
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, "error initializing client: %s\n", err) fmt.Fprintf(os.Stderr, "error initializing client: %s\n", err)
os.Exit(2) os.Exit(2)

@ -5,6 +5,7 @@ import (
"net/url" "net/url"
"os" "os"
"path/filepath" "path/filepath"
"strings"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/spf13/viper" "github.com/spf13/viper"
@ -33,7 +34,7 @@ $ salty-chat lookup {{ .Addr }}`
type setupCtx struct { type setupCtx struct {
Config saltyim.Config Config saltyim.Config
Addr saltyim.Addr Addr *saltyim.Addr
} }
var makeuserCmd = &cobra.Command{ var makeuserCmd = &cobra.Command{
@ -50,10 +51,16 @@ A valid top-level domain or sub-domain is required and the <user> is in the form
username@domain`, username@domain`,
Args: cobra.ExactArgs(1), Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
user := args[0]
endpoint := viper.GetString("endpoint")
identity := viper.GetString("identity") identity := viper.GetString("identity")
makeuser(identity, endpoint, user)
me := &saltyim.Addr{}
if sp := strings.Split(args[0], "@"); len(sp) > 1 {
me.User = sp[0]
me.Domain = sp[1]
}
// XXX: What if me.IsZero()
makeuser(me, identity)
}, },
} }
@ -61,19 +68,10 @@ func init() {
rootCmd.AddCommand(makeuserCmd) rootCmd.AddCommand(makeuserCmd)
} }
func makeuser(identity, endpoint, user string) { func makeuser(me *saltyim.Addr, identity string) {
if endpoint == "" { // XXX: Is there a better way to do this?
fmt.Fprintf(os.Stderr, "error no endpoint supplied\n") endpoint, _ := url.Parse(fmt.Sprintf("https://%s", me.Domain))
os.Exit(2) endpoint.Path = fmt.Sprintf("/%s", saltyim.MustGenerateULID())
}
endpointURL, err := url.Parse(endpoint)
if err != nil {
fmt.Fprintf(os.Stderr, "error parsing endpoint: %s\n", err)
os.Exit(2)
}
endpointURL.Path = fmt.Sprintf("/%s", saltyim.MustGenerateULID())
dir := filepath.Dir(identity) dir := filepath.Dir(identity)
if err := os.MkdirAll(dir, 0700); err != nil { if err := os.MkdirAll(dir, 0700); err != nil {
@ -81,20 +79,20 @@ func makeuser(identity, endpoint, user string) {
os.Exit(2) os.Exit(2)
} }
if err := saltyim.CreateIdentity(identity, user); err != nil { if err := saltyim.CreateIdentity(identity, me.String()); err != nil {
fmt.Fprintf(os.Stderr, "error creating identity %q for %s: %s\n", identity, user, err) fmt.Fprintf(os.Stderr, "error creating identity %q for %s: %s\n", identity, me, err)
os.Exit(2) os.Exit(2)
} }
key, me, err := saltyim.GetIdentity(identity) key, _, err := saltyim.GetIdentity(identity)
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, "error reading identity %s for %s: %s\n", identity, user, err) fmt.Fprintf(os.Stderr, "error reading identity %s for %s: %s\n", identity, me, err)
os.Exit(2) os.Exit(2)
} }
ctx := setupCtx{ ctx := setupCtx{
Config: saltyim.Config{ Config: saltyim.Config{
Endpoint: endpointURL.String(), Endpoint: endpoint.String(),
Key: key.PublicKey().ID().String(), Key: key.PublicKey().ID().String(),
}, },
Addr: me, Addr: me,

@ -22,23 +22,22 @@ var readCmd = &cobra.Command{
not specified defaults to the local user ($USER)`, not specified defaults to the local user ($USER)`,
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
user := viper.GetString("user") user := viper.GetString("user")
endpoint := viper.GetString("endpoint")
identity := viper.GetString("identity") identity := viper.GetString("identity")
var profiles []profile var profiles []profile
viper.UnmarshalKey("profiles", &profiles) viper.UnmarshalKey("profiles", &profiles)
for _, p := range profiles { for _, p := range profiles {
if user == p.User { if user == p.User {
endpoint = p.Endpoint
identity = p.Identity identity = p.Identity
} }
} }
var me saltyim.Addr me := &saltyim.Addr{}
if sp := strings.Split(user, "@"); len(sp) > 1 { if sp := strings.Split(user, "@"); len(sp) > 1 {
me.User = sp[0] me.User = sp[0]
me.Domain = sp[1] me.Domain = sp[1]
} }
// XXX: What if me.IsZero()
prehook, err := cmd.Flags().GetString("pre-hook") prehook, err := cmd.Flags().GetString("pre-hook")
if err != nil { if err != nil {
@ -50,13 +49,12 @@ not specified defaults to the local user ($USER)`,
log.Fatal("error getting --post-hook flag") log.Fatal("error getting --post-hook flag")
} }
read(me, identity, endpoint, prehook, posthook, args...) read(me, identity, prehook, posthook, args...)
}, },
} }
type profile struct { type profile struct {
User string User string
Endpoint string
Identity string Identity string
} }
@ -74,8 +72,8 @@ func init() {
) )
} }
func read(me saltyim.Addr, identity, endpoint string, prehook, posthook string, args ...string) { func read(me *saltyim.Addr, identity string, prehook, posthook string, args ...string) {
cli, err := saltyim.NewClient(me, identity, endpoint) cli, err := saltyim.NewClient(me, identity)
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, "error initializing client: %s\n", err) fmt.Fprintf(os.Stderr, "error initializing client: %s\n", err)
os.Exit(2) os.Exit(2)
@ -92,7 +90,7 @@ func read(me saltyim.Addr, identity, endpoint string, prehook, posthook string,
cancel() cancel()
}() }()
for msg := range cli.Read(ctx, endpoint, prehook, posthook) { for msg := range cli.Read(ctx, prehook, posthook) {
fmt.Println(saltyim.FormatMessage(msg)) fmt.Println(saltyim.FormatMessage(msg))
} }
} }

@ -17,33 +17,31 @@ var registerCmd = &cobra.Command{
Short: "Registers a new account with a broker", Short: "Registers a new account with a broker",
Long: `This command registers a new account with a broker. Long: `This command registers a new account with a broker.
A request is sent to the broker to the registration endpoint with the contents TBD`,
of the user's public key and salty address, signed with the user's private key.
If the broker can verify the request was signed correctly by the user a new
account is created and a Well-Known COnfing and Inbox created.`,
Args: cobra.ExactArgs(0), Args: cobra.ExactArgs(0),
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
fmt.Fprintln(os.Stderr, "✋ This is being re-designed. Stay tuned! 🤗")
os.Exit(1)
user := viper.GetString("user") user := viper.GetString("user")
endpoint := viper.GetString("endpoint")
identity := viper.GetString("identity") identity := viper.GetString("identity")
var profiles []profile var profiles []profile
viper.UnmarshalKey("profiles", &profiles) viper.UnmarshalKey("profiles", &profiles)
for _, p := range profiles { for _, p := range profiles {
if user == p.User { if user == p.User {
endpoint = p.Endpoint
identity = p.Identity identity = p.Identity
} }
} }
var me saltyim.Addr me := &saltyim.Addr{}
if sp := strings.Split(user, "@"); len(sp) > 1 { if sp := strings.Split(user, "@"); len(sp) > 1 {
me.User = sp[0] me.User = sp[0]
me.Domain = sp[1] me.Domain = sp[1]
} }
// XXX: What if me.IsZero()
register(me, identity, endpoint) register(me, identity)
}, },
} }
@ -51,15 +49,15 @@ func init() {
rootCmd.AddCommand(registerCmd) rootCmd.AddCommand(registerCmd)
} }
func register(me saltyim.Addr, identity, endpoint string) { func register(me *saltyim.Addr, identity string) {
cli, err := saltyim.NewClient(me, identity, endpoint) cli, err := saltyim.NewClient(me, identity)
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, "error initializing client: %s\n", err) fmt.Fprintf(os.Stderr, "error initializing client: %s\n", err)
os.Exit(2) os.Exit(2)
} }
if err := cli.Register(); err != nil { if err := cli.Register(); err != nil {
fmt.Fprintf(os.Stderr, "error registering to %s: %s\n", endpoint, err) fmt.Fprintf(os.Stderr, "error registering: %s\n", err)
os.Exit(2) os.Exit(2)
} }
fmt.Println("Success!") fmt.Println("Success!")

@ -7,6 +7,7 @@ import (
"strings" "strings"
"github.com/mitchellh/go-homedir" "github.com/mitchellh/go-homedir"
sync "github.com/sasha-s/go-deadlock"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/spf13/viper" "github.com/spf13/viper"
@ -36,6 +37,9 @@ See https://salty.im for more details.`,
log.SetLevel(log.DebugLevel) log.SetLevel(log.DebugLevel)
} else { } else {
log.SetLevel(log.InfoLevel) log.SetLevel(log.InfoLevel)
// Disable deadlock detection in production mode
sync.Opts.Disable = true
} }
}, },
} }
@ -73,11 +77,6 @@ func init() {
"Use the identity file at PATH", "Use the identity file at PATH",
) )
rootCmd.PersistentFlags().StringP(
"endpoint", "e", saltyim.DefaultEndpoint(),
"URI to connect to saltyim endpoint",
)
viper.BindPFlag("debug", rootCmd.PersistentFlags().Lookup("debug")) viper.BindPFlag("debug", rootCmd.PersistentFlags().Lookup("debug"))
viper.SetDefault("debug", false) viper.SetDefault("debug", false)
@ -87,14 +86,11 @@ func init() {
viper.BindPFlag("identity", rootCmd.PersistentFlags().Lookup("identity")) viper.BindPFlag("identity", rootCmd.PersistentFlags().Lookup("identity"))
viper.SetDefault("identity", saltyim.DefaultIdentity()) viper.SetDefault("identity", saltyim.DefaultIdentity())
viper.BindPFlag("endpoint", rootCmd.PersistentFlags().Lookup("endpoint")) viper.BindPFlag("pre-hook", rootCmd.PersistentFlags().Lookup("pre-hook"))
viper.SetDefault("endpoint", saltyim.DefaultEndpoint()) viper.SetDefault("pre-hook", "")
viper.BindPFlag("pre-hook", rootCmd.PersistentFlags().Lookup("pre-hook")) viper.BindPFlag("pre-hook", rootCmd.PersistentFlags().Lookup("pre-hook"))
viper.SetDefault("endpoint", saltyim.DefaultEndpoint()) viper.SetDefault("post-hook", "")
viper.BindPFlag("pre-hook", rootCmd.PersistentFlags().Lookup("pre-hook"))
viper.SetDefault("endpoint", saltyim.DefaultEndpoint())
} }
// initConfig reads in config file and ENV variables if set. // initConfig reads in config file and ENV variables if set.

@ -22,7 +22,7 @@ var sendCmd = &cobra.Command{
Short: "Send a message to a user", Short: "Send a message to a user",
Long: `This command attempts to lookup the user's Salty Config by using Long: `This command attempts to lookup the user's Salty Config by using
the Salty IM Discovery process by making a request to the user's Well-Known URI the Salty IM Discovery process by making a request to the user's Well-Known URI
After it will attempt to send the message via a post to the endpoint. After it will attempt to send the message via a HTTP POST to their Endpoint.
The User is expected to have a Configuration file located at a Well-Known URI of: The User is expected to have a Configuration file located at a Well-Known URI of:
/.well-known/salty/<sha256hex(user@domain.tld)>.json /.well-known/salty/<sha256hex(user@domain.tld)>.json
@ -35,25 +35,24 @@ https://mills.io/.well-known/salty/prologic.json`,
Args: cobra.MinimumNArgs(1), Args: cobra.MinimumNArgs(1),
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
user := viper.GetString("user") user := viper.GetString("user")
endpoint := viper.GetString("endpoint")
identity := viper.GetString("identity") identity := viper.GetString("identity")
var profiles []profile var profiles []profile
viper.UnmarshalKey("profiles", &profiles) viper.UnmarshalKey("profiles", &profiles)
for _, p := range profiles { for _, p := range profiles {
if user == p.User { if user == p.User {
endpoint = p.Endpoint
identity = p.Identity identity = p.Identity
} }
} }
var me saltyim.Addr me := &saltyim.Addr{}
if sp := strings.Split(user, "@"); len(sp) > 1 { if sp := strings.Split(user, "@"); len(sp) > 1 {
me.User = sp[0] me.User = sp[0]
me.Domain = sp[1] me.Domain = sp[1]
} }
// XXX: What if me.IsZero()
send(me, identity, endpoint, args[0], args[1:]...) send(me, identity, args[0], args[1:]...)
}, },
} }
@ -61,14 +60,14 @@ func init() {
rootCmd.AddCommand(sendCmd) rootCmd.AddCommand(sendCmd)
} }
func send(me saltyim.Addr, identity, endpoint, user string, args ...string) { func send(me *saltyim.Addr, identity, user string, args ...string) {
user = strings.TrimSpace(user) user = strings.TrimSpace(user)
if user == "" { if user == "" {
fmt.Fprintf(os.Stderr, "error: no user supplied\n") fmt.Fprintf(os.Stderr, "error: no user supplied\n")
os.Exit(2) os.Exit(2)
} }
cli, err := saltyim.NewClient(me, identity, endpoint) cli, err := saltyim.NewClient(me, identity)
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, "error initializing client: %s\n", err) fmt.Fprintf(os.Stderr, "error initializing client: %s\n", err)
os.Exit(2) os.Exit(2)

@ -9,6 +9,7 @@ import (
"path/filepath" "path/filepath"
"strings" "strings"
sync "github.com/sasha-s/go-deadlock"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
flag "github.com/spf13/pflag" flag "github.com/spf13/pflag"
profiler "github.com/wblakecaldwell/profiler" profiler "github.com/wblakecaldwell/profiler"
@ -88,6 +89,9 @@ func main() {
log.SetLevel(log.DebugLevel) log.SetLevel(log.DebugLevel)
} else { } else {
log.SetLevel(log.InfoLevel) log.SetLevel(log.InfoLevel)
// Disable deadlock detection in production mode
sync.Opts.Disable = true
} }
svr, err := internal.NewServer(bind, svr, err := internal.NewServer(bind,

@ -8,6 +8,7 @@ import (
"github.com/logrusorgru/aurora" "github.com/logrusorgru/aurora"
"github.com/posener/formatter" "github.com/posener/formatter"
log "github.com/sirupsen/logrus"
"go.yarn.social/lextwt" "go.yarn.social/lextwt"
) )
@ -36,7 +37,7 @@ func FormatMessage(msg string) string {
st, ok := s.(*lextwt.SaltyText) st, ok := s.(*lextwt.SaltyText)
if !ok { if !ok {
fmt.Fprintf(os.Stderr, "unexpected error") log.Errorf("unexpected error, expected type lextwt.SaltyText got #%v", st)
return "" return ""
} }

2
go.mod

@ -5,6 +5,7 @@ go 1.17
require ( require (
github.com/mitchellh/go-homedir v1.1.0 github.com/mitchellh/go-homedir v1.1.0
github.com/oklog/ulid/v2 v2.0.2 github.com/oklog/ulid/v2 v2.0.2
github.com/sasha-s/go-deadlock v0.3.1
github.com/sirupsen/logrus v1.8.1 github.com/sirupsen/logrus v1.8.1
github.com/spf13/cobra v1.4.0 github.com/spf13/cobra v1.4.0
github.com/spf13/viper v1.10.1 github.com/spf13/viper v1.10.1
@ -37,6 +38,7 @@ require (
github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
github.com/mitchellh/mapstructure v1.4.3 // indirect github.com/mitchellh/mapstructure v1.4.3 // indirect
github.com/pelletier/go-toml v1.9.4 // indirect github.com/pelletier/go-toml v1.9.4 // indirect
github.com/petermattis/goid v0.0.0-20180202154549-b0b1615b78e5 // indirect
github.com/pkg/errors v0.9.1 // indirect github.com/pkg/errors v0.9.1 // indirect
github.com/plar/go-adaptive-radix-tree v1.0.4 // indirect github.com/plar/go-adaptive-radix-tree v1.0.4 // indirect
github.com/prometheus/client_golang v1.12.1 // indirect github.com/prometheus/client_golang v1.12.1 // indirect

7
go.sum

@ -415,6 +415,8 @@ github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/9
github.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= github.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
github.com/pelletier/go-toml v1.9.4 h1:tjENF6MfZAg8e4ZmZTeWaWiT2vXtsoO6+iuOjFhECwM= github.com/pelletier/go-toml v1.9.4 h1:tjENF6MfZAg8e4ZmZTeWaWiT2vXtsoO6+iuOjFhECwM=
github.com/pelletier/go-toml v1.9.4/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= github.com/pelletier/go-toml v1.9.4/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
github.com/petermattis/goid v0.0.0-20180202154549-b0b1615b78e5 h1:q2e307iGHPdTGp0hoxKjt1H5pDo6utceo3dQVK3I5XQ=
github.com/petermattis/goid v0.0.0-20180202154549-b0b1615b78e5/go.mod h1:jvVRKCrJTQWu0XVbaOlby/2lO20uSCHEMzzplHXte1o=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
@ -479,6 +481,8 @@ github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQD
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
github.com/sagikazarmark/crypt v0.4.0/go.mod h1:ALv2SRj7GxYV4HO9elxH9nS6M9gW+xDNxqmyJ6RfDFM= github.com/sagikazarmark/crypt v0.4.0/go.mod h1:ALv2SRj7GxYV4HO9elxH9nS6M9gW+xDNxqmyJ6RfDFM=
github.com/sasha-s/go-deadlock v0.3.1 h1:sqv7fDNShgjcaxkO0JNcOAlr8B9+cV5Ey/OB71efZx0=
github.com/sasha-s/go-deadlock v0.3.1/go.mod h1:F73l+cr82YSh10GxyRI6qZiCgK64VaZjwesgfQ1/iLM=
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
@ -565,8 +569,6 @@ go.etcd.io/etcd/client/pkg/v3 v3.5.0/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3
go.etcd.io/etcd/client/pkg/v3 v3.5.1/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= go.etcd.io/etcd/client/pkg/v3 v3.5.1/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g=
go.etcd.io/etcd/client/v2 v2.305.0/go.mod h1:h9puh54ZTgAKtEbut2oe9P4L/oqKCVB6xsXlzd7alYQ= go.etcd.io/etcd/client/v2 v2.305.0/go.mod h1:h9puh54ZTgAKtEbut2oe9P4L/oqKCVB6xsXlzd7alYQ=
go.etcd.io/etcd/client/v2 v2.305.1/go.mod h1:pMEacxZW7o8pg4CrFE7pquyCJJzZvkvdD2RibOCCCGs= go.etcd.io/etcd/client/v2 v2.305.1/go.mod h1:pMEacxZW7o8pg4CrFE7pquyCJJzZvkvdD2RibOCCCGs=
go.mills.io/salty v0.0.0-20220318125419-fb3d6fc9e870 h1:fH4ftkY8i0Y2ycstDXmVmqxKyY+l4Gx4OvgxBm/wk8Q=
go.mills.io/salty v0.0.0-20220318125419-fb3d6fc9e870/go.mod h1:bQ9yvK7wwThD4tzoioJq/YAuwYOB2XA9tAUHIYtjre8=
go.mills.io/salty v0.0.0-20220322161301-ce2b9f6573fa h1:KBxzYJMWP7MXd72RgqsMCGOSEqV6aaDDSdSb8usJCzQ= go.mills.io/salty v0.0.0-20220322161301-ce2b9f6573fa h1:KBxzYJMWP7MXd72RgqsMCGOSEqV6aaDDSdSb8usJCzQ=
go.mills.io/salty v0.0.0-20220322161301-ce2b9f6573fa/go.mod h1:bQ9yvK7wwThD4tzoioJq/YAuwYOB2XA9tAUHIYtjre8= go.mills.io/salty v0.0.0-20220322161301-ce2b9f6573fa/go.mod h1:bQ9yvK7wwThD4tzoioJq/YAuwYOB2XA9tAUHIYtjre8=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
@ -1043,7 +1045,6 @@ google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGj
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+RurQ=
google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw= google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw=
google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=

@ -1,4 +1,3 @@
#!/bin/sh #!/bin/sh
cat >> msgs cat >> msgs
echo >> msgs

@ -12,20 +12,21 @@ import (
"go.mills.io/salty" "go.mills.io/salty"
) )
func readUser(fd io.Reader) (Addr, error) { func readUser(fd io.Reader) (*Addr, error) {
scan := bufio.NewScanner(fd) scan := bufio.NewScanner(fd)
var a Addr addr := &Addr{}
for scan.Scan() { for scan.Scan() {
if strings.HasPrefix(scan.Text(), "# user:") { if strings.HasPrefix(scan.Text(), "# user:") {
user := strings.Split(strings.TrimSpace(strings.TrimPrefix(scan.Text(), "# user:")), "@") user := strings.Split(strings.TrimSpace(strings.TrimPrefix(scan.Text(), "# user:")), "@")
if len(user) != 2 { if len(user) != 2 {
return Addr{}, nil return nil, nil
} }
a.User, a.Domain = user[0], user[1] addr.User, addr.Domain = user[0], user[1]
} }
} }
return a, scan.Err() return addr, scan.Err()
} }
// DefaultIdentity returns a default identity file (if one exists) otherwise // DefaultIdentity returns a default identity file (if one exists) otherwise
@ -64,7 +65,7 @@ func CreateIdentity(fn, user string) error {
} }
// GetIdentity ... // GetIdentity ...
func GetIdentity(fn string) (*keys.EdX25519Key, Addr, error) { func GetIdentity(fn string) (*keys.EdX25519Key, *Addr, error) {
// Handle unix home with `~` // Handle unix home with `~`
if strings.HasPrefix(fn, "~/") { if strings.HasPrefix(fn, "~/") {
dirname, _ := os.UserHomeDir() dirname, _ := os.UserHomeDir()
@ -73,13 +74,13 @@ func GetIdentity(fn string) (*keys.EdX25519Key, Addr, error) {
id, err := os.Open(fn) id, err := os.Open(fn)
if err != nil { if err != nil {
return nil, Addr{}, fmt.Errorf("error opening identity %q: %s", fn, err) return nil, nil, fmt.Errorf("error opening identity %q: %s", fn, err)
} }
defer id.Close() defer id.Close()
key, err := salty.ParseIdentity(id) key, err := salty.ParseIdentity(id)
if err != nil { if err != nil {
return nil, Addr{}, fmt.Errorf("error reading identity %q: %s", fn, err) return nil, nil, fmt.Errorf("error reading identity %q: %s", fn, err)
} }
id.Seek(0, 0) id.Seek(0, 0)

@ -5,13 +5,13 @@ import (
"context" "context"
"fmt" "fmt"
"strings" "strings"
"sync"
"github.com/dim13/crc24" "github.com/dim13/crc24"
"github.com/gdamore/tcell/v2" "github.com/gdamore/tcell/v2"
"github.com/gdamore/tcell/v2/encoding" "github.com/gdamore/tcell/v2/encoding"
"github.com/posener/formatter" "github.com/posener/formatter"
"github.com/rivo/tview" "github.com/rivo/tview"
sync "github.com/sasha-s/go-deadlock"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"go.yarn.social/lextwt" "go.yarn.social/lextwt"
@ -31,7 +31,6 @@ type ChatTUI struct {
mu sync.RWMutex mu sync.RWMutex
cli *saltyim.Client cli *saltyim.Client
me saltyim.Addr
user string user string
config saltyim.Config config saltyim.Config
@ -129,15 +128,13 @@ func (c *ChatTUI) SetScreen(inCh <-chan string, outCh chan<- string) {
app := tview.NewApplication() app := tview.NewApplication()
title := fmt.Sprintf( chatTitle := fmt.Sprintf("Chatting to %s via %s", c.user, c.config.Endpoint)
"Chatting as %s via %s with %s via %s", inputTitle := fmt.Sprintf("Connected to %s as %s", c.cli.Me().Endpoint(), c.cli.Me())
c.cli.Me(), c.cli.Endpoint(), c.user, c.config.Endpoint,
)
// Generate UI components. // Generate UI components.
c.mu.RLock() c.mu.RLock()
chatBox := NewChatBox(c.palette, title) chatBox := NewChatBox(c.palette, chatTitle)
inputField := NewChatInput(c.palette, c.newMessageHandler(outCh)) inputField := NewChatInput(c.palette, inputTitle, c.newMessageHandler(outCh))
c.mu.RUnlock() c.mu.RUnlock()
// Layout the widgets in flex view. // Layout the widgets in flex view.
@ -160,7 +157,7 @@ func (c *ChatTUI) RunChat(inCh chan<- string, outCh <-chan string) {
// Receives incoming messages on a separate goroutine to be non-blocking. // Receives incoming messages on a separate goroutine to be non-blocking.
go func() { go func() {
for msg := range c.cli.Read(ctx, "", "", "") { for msg := range c.cli.Read(ctx, "", "") {
inCh <- msg inCh <- msg
} }
}() }()

@ -38,11 +38,13 @@ func NewChatBox(palette map[string]string, title string) *tview.TextView {
// NewChatInput initializes and returns a 'chatInput' component // NewChatInput initializes and returns a 'chatInput' component
// that handles user inputs and forwards chat messages. // that handles user inputs and forwards chat messages.
func NewChatInput(palette map[string]string, handler InputHandler) *tview.InputField { func NewChatInput(palette map[string]string, title string, handler InputHandler) *tview.InputField {
nLine := regexp.MustCompile(`\\n\s?`) nLine := regexp.MustCompile(`\\n\s?`)
// Initialize 'inputField' for user inputs. // Initialize 'inputField' for user inputs.
chatInput := tview.NewInputField() chatInput := tview.NewInputField()
chatInput.SetTitle(title).
SetTitleColor(hexToTCell(palette["title"]))
chatInput.SetFieldBackgroundColor(tcell.ColorBlack). chatInput.SetFieldBackgroundColor(tcell.ColorBlack).
SetFieldWidth(0). SetFieldWidth(0).
SetFieldTextColor(hexToTCell(palette["text"])). SetFieldTextColor(hexToTCell(palette["text"])).

@ -7,64 +7,109 @@ import (
"io/ioutil" "io/ioutil"
"net" "net"
"net/http" "net/http"
"net/url"
"strings" "strings"
sync "github.com/sasha-s/go-deadlock"
) )
// Addr represents a Salty IM User's Address // Addr represents a Salty IM User's Address
type Addr struct { type Addr struct {
mu sync.RWMutex
User string User string
Domain string Domain string
DiscoveryDomain string
endpoint *url.URL
discoveredDomain string
} }
// IsZero returns true if the address is empty // IsZero returns true if the address is empty
func (a Addr) IsZero() bool { func (a *Addr) IsZero() bool {
a.mu.RLock()
defer a.mu.RUnlock()
return a.User == "" && a.Domain == "" return a.User == "" && a.Domain == ""
} }
func (a Addr) String() string { func (a *Addr) String() string {
return fmt.Sprintf("%s@%s", a.User, a.Domain) return fmt.Sprintf("%s@%s", a.User, a.Domain)
} }
// Hash returns the Hex(SHA256Sum()) of the Address // Hash returns the Hex(SHA256Sum()) of the Address
func (a Addr) Hash() string { func (a *Addr) Hash() string {
return fmt.Sprintf("%x", sha256.Sum256([]byte(a.String()))) return fmt.Sprintf("%x", sha256.Sum256([]byte(a.String())))
} }
// Formatted returns a formatted user used in the Salty Message Format // Formatted returns a formatted user used in the Salty Message Format
// <timestamp\t(<user>) <message>\n // <timestamp\t(<user>) <message>\n
func (a Addr) Formatted() string { func (a *Addr) Formatted() string {
return fmt.Sprintf("(%s)", a) return fmt.Sprintf("(%s)", a)
} }
// Endpoint returns the discovered Endpoint
func (a *Addr) Endpoint() *url.URL {
a.mu.RLock()
defer a.mu.RUnlock()
return a.endpoint
}
// DiscoveredDomain returns the discovered domain (if any) of fallbacks to the Domain
func (a *Addr) DiscoveredDomain() string {
a.mu.RLock()
defer a.mu.RUnlock()
if a.discoveredDomain != "" {
return a.discoveredDomain
}
return a.Domain
}
// URI returns the Well-Known URI for this Addr // URI returns the Well-Known URI for this Addr
func (a Addr) URI() string { func (a *Addr) URI() string {
return fmt.Sprintf("https://%s/.well-known/salty/%s.json", a.DiscoveryDomain, a.User) return fmt.Sprintf("https://%s/.well-known/salty/%s.json", a.DiscoveredDomain(), a.User)
} }
// HashURI returns the Well-Known HashURI for this Addr // HashURI returns the Well-Known HashURI for this Addr
func (a Addr) HashURI() string { func (a *Addr) HashURI() string {
return fmt.Sprintf("https://%s/.well-known/salty/%s.json", a.DiscoveryDomain, a.Hash()) return fmt.Sprintf("https://%s/.well-known/salty/%s.json", a.DiscoveredDomain(), a.Hash())
} }
func (a *Addr) RefreshDiscovery() { func (a *Addr) Refresh() error {
a.mu.Lock()
defer a.mu.Unlock()
_, records, err := net.LookupSRV("salty", "tcp", a.Domain) _, records, err := net.LookupSRV("salty", "tcp", a.Domain)
if err != nil || len(records) == 0 { if err == nil && len(records) > 0 {
return a.discoveredDomain = records[0].Target
} }
a.DiscoveryDomain = records[0].Target
config, err := Lookup(a.String())
if err != nil {
return fmt.Errorf("error looking up endpoint for %s: %w", a, err)
}
u, err := url.Parse(config.Endpoint)
if err != nil {
return fmt.Errorf("error parsing endpoint %s: %w", config.Endpoint, err)
}
a.endpoint = u
return nil
} }
// ParseAddr parsers a Salty Address for a user into it's user and domain // ParseAddr parsers a Salty Address for a user into it's user and domain
// parts and returns an Addr object with the User and Domain and a method // parts and returns an Addr object with the User and Domain and a method
// for returning the expected User's Well-Known URI // for returning the expected User's Well-Known URI
func ParseAddr(addr string) (Addr, error) { func ParseAddr(addr string) (*Addr, error) {
parts := strings.Split(addr, "@") parts := strings.Split(addr, "@")
if len(parts) != 2 { if len(parts) != 2 {
return Addr{}, fmt.Errorf("expected nick@domain found %q", addr) return nil, fmt.Errorf("expected nick@domain found %q", addr)
} }
return Addr{User: parts[0], Domain: parts[1], DiscoveryDomain: parts[1]}, nil return &Addr{User: parts[0], Domain: parts[1]}, nil
} }
// Lookup looks up a Salty Address for a User by parsing the user's domain and // Lookup looks up a Salty Address for a User by parsing the user's domain and
@ -75,7 +120,6 @@ func Lookup(addr string) (Config, error) {
if err != nil { if err != nil {
return Config{}, err return Config{}, err
} }
a.RefreshDiscovery()
config, err := fetchConfig(a.HashURI()) config, err := fetchConfig(a.HashURI())
if err != nil { if err != nil {
// Fallback to plain user nick // Fallback to plain user nick