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

View File

@ -3,10 +3,8 @@ package saltyim
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"net/url"
"os"
"time"
@ -23,7 +21,7 @@ type configCache map[string]Config
// PackMessage formts an outoing message in the Message Format
// <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))
}
@ -41,40 +39,38 @@ func Send(endpoint, msg string) error {
// Client is a Salty IM client that handles talking to a Salty IM Broker
// and Sedngina and Receiving messages to/from Salty IM Users.
type Client struct {
key *keys.EdX25519Key
endpoint string
me Addr
me *Addr
key *keys.EdX25519Key
cache configCache
}
// NewClient reeturns a new Salty IM client for sending and receiving
// encrypted messages to other Salty IM users as well as decrypting
// 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)
if err != nil {
return nil, fmt.Errorf("error opening identity %s: %w", identity, err)
}
if me.IsZero() {
if me == nil || me.IsZero() {
me = m
}
if me.IsZero() {
if me == nil || me.IsZero() {
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("Salty Addr is %s", me)
log.Debugf("Endpoint is %s", endpoint)
log.Debugf("Endpoint is %s", me.Endpoint())
return &Client{
key: key,
endpoint: endpoint,
me: me,
me: me,
key: key,
cache: make(configCache),
}, 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) Endpoint() string { return cli.endpoint }
func (cli *Client) Me() *Addr { return cli.me }
func (cli *Client) Key() *keys.EdX25519PublicKey { return cli.key.PublicKey() }
// Read subscribers to this user's inbox for new messages
func (cli *Client) Read(ctx context.Context, endpoint, prehook, posthook string) chan string {
if endpoint == "" {
endpoint = cli.endpoint
}
uri, inbox := SplitInbox(endpoint)
func (cli *Client) Read(ctx context.Context, prehook, posthook string) chan string {
uri, inbox := SplitInbox(cli.me.Endpoint().String())
bus := msgbus_client.NewClient(uri, nil)
msgs := make(chan string)
@ -173,31 +165,5 @@ func (cli *Client) Send(user, msg string) error {
// Register sends a registration requestn to a broker
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
}

View File

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

View File

@ -5,6 +5,7 @@ import (
"net/url"
"os"
"path/filepath"
"strings"
"github.com/spf13/cobra"
"github.com/spf13/viper"
@ -33,7 +34,7 @@ $ salty-chat lookup {{ .Addr }}`
type setupCtx struct {
Config saltyim.Config
Addr saltyim.Addr
Addr *saltyim.Addr
}
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`,
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
user := args[0]
endpoint := viper.GetString("endpoint")
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)
}
func makeuser(identity, endpoint, user string) {
if endpoint == "" {
fmt.Fprintf(os.Stderr, "error no endpoint supplied\n")
os.Exit(2)
}
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())
func makeuser(me *saltyim.Addr, identity string) {
// XXX: Is there a better way to do this?
endpoint, _ := url.Parse(fmt.Sprintf("https://%s", me.Domain))
endpoint.Path = fmt.Sprintf("/%s", saltyim.MustGenerateULID())
dir := filepath.Dir(identity)
if err := os.MkdirAll(dir, 0700); err != nil {
@ -81,20 +79,20 @@ func makeuser(identity, endpoint, user string) {
os.Exit(2)
}
if err := saltyim.CreateIdentity(identity, user); err != nil {
fmt.Fprintf(os.Stderr, "error creating identity %q for %s: %s\n", identity, user, err)
if err := saltyim.CreateIdentity(identity, me.String()); err != nil {
fmt.Fprintf(os.Stderr, "error creating identity %q for %s: %s\n", identity, me, err)
os.Exit(2)
}
key, me, err := saltyim.GetIdentity(identity)
key, _, err := saltyim.GetIdentity(identity)
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)
}
ctx := setupCtx{
Config: saltyim.Config{
Endpoint: endpointURL.String(),
Endpoint: endpoint.String(),
Key: key.PublicKey().ID().String(),
},
Addr: me,

View File

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

View File

@ -17,33 +17,31 @@ var registerCmd = &cobra.Command{
Short: "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
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.`,
TBD`,
Args: cobra.ExactArgs(0),
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")
endpoint := viper.GetString("endpoint")
identity := viper.GetString("identity")
var profiles []profile
viper.UnmarshalKey("profiles", &profiles)
for _, p := range profiles {
if user == p.User {
endpoint = p.Endpoint
identity = p.Identity
}
}
var me saltyim.Addr
me := &saltyim.Addr{}
if sp := strings.Split(user, "@"); len(sp) > 1 {
me.User = sp[0]
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)
}
func register(me saltyim.Addr, identity, endpoint string) {
cli, err := saltyim.NewClient(me, identity, endpoint)
func register(me *saltyim.Addr, identity string) {
cli, err := saltyim.NewClient(me, identity)
if err != nil {
fmt.Fprintf(os.Stderr, "error initializing client: %s\n", err)
os.Exit(2)
}
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)
}
fmt.Println("Success!")

View File

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

View File

@ -22,7 +22,7 @@ var sendCmd = &cobra.Command{
Short: "Send a message to a user",
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
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:
/.well-known/salty/<sha256hex(user@domain.tld)>.json
@ -35,25 +35,24 @@ https://mills.io/.well-known/salty/prologic.json`,
Args: cobra.MinimumNArgs(1),
Run: func(cmd *cobra.Command, args []string) {
user := viper.GetString("user")
endpoint := viper.GetString("endpoint")
identity := viper.GetString("identity")
var profiles []profile
viper.UnmarshalKey("profiles", &profiles)
for _, p := range profiles {
if user == p.User {
endpoint = p.Endpoint
identity = p.Identity
}
}
var me saltyim.Addr
me := &saltyim.Addr{}
if sp := strings.Split(user, "@"); len(sp) > 1 {
me.User = sp[0]
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)
}
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)
if user == "" {
fmt.Fprintf(os.Stderr, "error: no user supplied\n")
os.Exit(2)
}
cli, err := saltyim.NewClient(me, identity, endpoint)
cli, err := saltyim.NewClient(me, identity)
if err != nil {
fmt.Fprintf(os.Stderr, "error initializing client: %s\n", err)
os.Exit(2)

View File

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

View File

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

2
go.mod
View File

@ -5,6 +5,7 @@ go 1.17
require (
github.com/mitchellh/go-homedir v1.1.0
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/spf13/cobra v1.4.0
github.com/spf13/viper v1.10.1
@ -37,6 +38,7 @@ require (
github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
github.com/mitchellh/mapstructure v1.4.3 // 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/plar/go-adaptive-radix-tree v1.0.4 // indirect
github.com/prometheus/client_golang v1.12.1 // indirect

7
go.sum
View File

@ -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.4 h1:tjENF6MfZAg8e4ZmZTeWaWiT2vXtsoO6+iuOjFhECwM=
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/errors v0.8.0/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/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/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/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=
@ -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/v2 v2.305.0/go.mod h1:h9puh54ZTgAKtEbut2oe9P4L/oqKCVB6xsXlzd7alYQ=
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/go.mod h1:bQ9yvK7wwThD4tzoioJq/YAuwYOB2XA9tAUHIYtjre8=
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.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.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+RurQ=
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/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=

View File

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

View File

@ -12,20 +12,21 @@ import (
"go.mills.io/salty"
)
func readUser(fd io.Reader) (Addr, error) {
func readUser(fd io.Reader) (*Addr, error) {
scan := bufio.NewScanner(fd)
var a Addr
addr := &Addr{}
for scan.Scan() {
if strings.HasPrefix(scan.Text(), "# user:") {
user := strings.Split(strings.TrimSpace(strings.TrimPrefix(scan.Text(), "# user:")), "@")
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
@ -64,7 +65,7 @@ func CreateIdentity(fn, user string) error {
}
// GetIdentity ...
func GetIdentity(fn string) (*keys.EdX25519Key, Addr, error) {
func GetIdentity(fn string) (*keys.EdX25519Key, *Addr, error) {
// Handle unix home with `~`
if strings.HasPrefix(fn, "~/") {
dirname, _ := os.UserHomeDir()
@ -73,13 +74,13 @@ func GetIdentity(fn string) (*keys.EdX25519Key, Addr, error) {
id, err := os.Open(fn)
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()
key, err := salty.ParseIdentity(id)
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)

View File

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

View File

@ -38,11 +38,13 @@ func NewChatBox(palette map[string]string, title string) *tview.TextView {
// NewChatInput initializes and returns a 'chatInput' component
// 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?`)
// Initialize 'inputField' for user inputs.
chatInput := tview.NewInputField()
chatInput.SetTitle(title).
SetTitleColor(hexToTCell(palette["title"]))
chatInput.SetFieldBackgroundColor(tcell.ColorBlack).
SetFieldWidth(0).
SetFieldTextColor(hexToTCell(palette["text"])).

View File

@ -7,64 +7,109 @@ import (
"io/ioutil"
"net"
"net/http"
"net/url"
"strings"
sync "github.com/sasha-s/go-deadlock"
)
// Addr represents a Salty IM User's Address
type Addr struct {
mu sync.RWMutex
User string
Domain string
DiscoveryDomain string
endpoint *url.URL
discoveredDomain string
}
// 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 == ""
}
func (a Addr) String() string {
func (a *Addr) String() string {
return fmt.Sprintf("%s@%s", a.User, a.Domain)
}
// 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())))
}
// Formatted returns a formatted user used in the Salty Message Format
// <timestamp\t(<user>) <message>\n
func (a Addr) Formatted() string {
func (a *Addr) Formatted() string {
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
func (a Addr) URI() string {
return fmt.Sprintf("https://%s/.well-known/salty/%s.json", a.DiscoveryDomain, a.User)
func (a *Addr) URI() string {
return fmt.Sprintf("https://%s/.well-known/salty/%s.json", a.DiscoveredDomain(), a.User)
}
// HashURI returns the Well-Known HashURI for this Addr
func (a Addr) HashURI() string {
return fmt.Sprintf("https://%s/.well-known/salty/%s.json", a.DiscoveryDomain, a.Hash())
func (a *Addr) HashURI() string {
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)
if err != nil || len(records) == 0 {
return
if err == nil && len(records) > 0 {
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
// parts and returns an Addr object with the User and Domain and a method
// 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, "@")
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
@ -75,7 +120,6 @@ func Lookup(addr string) (Config, error) {
if err != nil {
return Config{}, err
}
a.RefreshDiscovery()
config, err := fetchConfig(a.HashURI())
if err != nil {
// Fallback to plain user nick