mirror of
https://git.mills.io/saltyim/saltyim.git
synced 2024-06-16 03:48:24 +00:00
Refactor the code (#38)
Co-authored-by: James Mills <prologic@shortcircuit.net.au> Reviewed-on: https://git.mills.io/prologic/saltyim/pulls/38
This commit is contained in:
parent
950f54708f
commit
8993981e81
298
client.go
298
client.go
@ -4,191 +4,167 @@ import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/dim13/crc24"
|
||||
"github.com/gdamore/tcell/v2"
|
||||
"github.com/gdamore/tcell/v2/encoding"
|
||||
"git.mills.io/prologic/msgbus"
|
||||
msgbus_client "git.mills.io/prologic/msgbus/client"
|
||||
"github.com/keys-pub/keys"
|
||||
"github.com/posener/formatter"
|
||||
"github.com/rivo/tview"
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"go.mills.io/salty"
|
||||
"go.yarn.social/lextwt"
|
||||
"go.mills.io/saltyim/internal/exec"
|
||||
)
|
||||
|
||||
const (
|
||||
dateTimeFormat = "2006-01-02 15:04:05"
|
||||
)
|
||||
type configCache map[string]Config
|
||||
|
||||
func getUserColor(s string) tcell.Color {
|
||||
c := crc24.Sum([]byte(s))
|
||||
return tcell.NewRGBColor(
|
||||
int32(c>>0x10),
|
||||
int32(c>>0x08),
|
||||
int32(c),
|
||||
)
|
||||
// PackMessage formts an outoing message in the Message Format
|
||||
// <timestamp>\t(<sender>) <message>
|
||||
func PackMessage(me Addr, msg string) []byte {
|
||||
return []byte(fmt.Sprint(time.Now().UTC().Format(time.RFC3339), "\t", me.Formatted(), "\t", msg))
|
||||
}
|
||||
|
||||
type chatClient struct {
|
||||
mu sync.RWMutex
|
||||
|
||||
key *keys.EdX25519Key
|
||||
me Addr
|
||||
endpoint string
|
||||
user string
|
||||
config Config
|
||||
|
||||
// Configurations.
|
||||
palette map[string]string
|
||||
}
|
||||
|
||||
// NewChatClient initializes a new chatClient.
|
||||
// Sets up connection with broker, and initializes UI.
|
||||
func NewChatClient(key *keys.EdX25519Key, me Addr, endpoint, user string) (*chatClient, error) {
|
||||
config, err := Lookup(user)
|
||||
// Send sends the encrypted message `msg` to the Endpoint `endpoint` using a
|
||||
// `POST` request and returns nil on success or an error on failure.
|
||||
func Send(endpoint, msg string) error {
|
||||
res, err := Request(http.MethodPost, endpoint, nil, bytes.NewBufferString(msg))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error: failed to lookup user %q: %w", user, err)
|
||||
return fmt.Errorf("error publishing message to %s: %w", endpoint, err)
|
||||
}
|
||||
defer res.Body.Close()
|
||||
return nil
|
||||
}
|
||||
|
||||
// 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
|
||||
|
||||
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) {
|
||||
key, m, err := GetIdentity(identity)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error opening identity %s: %w", identity, err)
|
||||
}
|
||||
if me.IsZero() {
|
||||
me = m
|
||||
}
|
||||
|
||||
return &chatClient{
|
||||
key: key,
|
||||
me: me,
|
||||
endpoint: endpoint,
|
||||
user: user,
|
||||
config: config,
|
||||
if me.IsZero() {
|
||||
return nil, fmt.Errorf("unable to find your user addressn in %s", identity)
|
||||
}
|
||||
|
||||
palette: map[string]string{
|
||||
"background": "000000",
|
||||
"border": "4C5C68",
|
||||
"title": "46494C",
|
||||
"date": "E76F51",
|
||||
"text": "577399",
|
||||
},
|
||||
log.Infof("Using identity %s with public key %s", identity, key)
|
||||
log.Infof("Salty Addr is %s", me)
|
||||
log.Infof("Endpoint is %s", endpoint)
|
||||
|
||||
return &Client{
|
||||
key: key,
|
||||
|
||||
endpoint: endpoint,
|
||||
me: me,
|
||||
|
||||
cache: make(configCache),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// newMessageHandler returns an InputHandler that handles outgoing messages.
|
||||
func (cc *chatClient) newMessageHandler(outCh chan<- string) InputHandler {
|
||||
handler := func(message string) {
|
||||
cc.mu.RLock()
|
||||
outCh <- message
|
||||
cc.mu.RUnlock()
|
||||
func (cli *Client) getConfig(user string) (Config, error) {
|
||||
config, ok := cli.cache[user]
|
||||
if ok {
|
||||
return config, nil
|
||||
}
|
||||
|
||||
return handler
|
||||
}
|
||||
|
||||
// updateChatBox updates chatBox component with incoming messages.
|
||||
func (cc *chatClient) updateChatBox(inCh <-chan string, app *tview.Application,
|
||||
chatBox *tview.TextView) {
|
||||
|
||||
for in := range inCh {
|
||||
s, err := lextwt.ParseSalty(in)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
switch s := s.(type) {
|
||||
|
||||
case *lextwt.SaltyEvent:
|
||||
// Ignored for now
|
||||
|
||||
case *lextwt.SaltyText:
|
||||
if s.User.String() != cc.me.String() && s.User.String() != cc.user {
|
||||
continue
|
||||
}
|
||||
|
||||
buf := &bytes.Buffer{}
|
||||
_, _, width, _ := chatBox.GetInnerRect()
|
||||
f := formatter.Formatter{
|
||||
Writer: buf,
|
||||
Indent: []byte("> "),
|
||||
Width: width - 1,
|
||||
}
|
||||
|
||||
if _, err := f.Write([]byte(s.LiteralText())); err != nil {
|
||||
log.WithError(err).Error("error formatting message")
|
||||
continue
|
||||
}
|
||||
|
||||
cc.mu.RLock()
|
||||
app.QueueUpdateDraw(func() {
|
||||
fmt.Fprintf(chatBox,
|
||||
"[#%s]%s [#%x]%s\n[#%s]%s\n\n",
|
||||
cc.palette["date"],
|
||||
s.Timestamp.DateTime().Local().Format(dateTimeFormat),
|
||||
getUserColor(s.User.String()).Hex(),
|
||||
s.User.String(),
|
||||
cc.palette["text"],
|
||||
buf.String(),
|
||||
)
|
||||
})
|
||||
cc.mu.RUnlock()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// setScreen initializes the layout and UI components.
|
||||
func (cc *chatClient) SetScreen(inCh <-chan string, outCh chan<- string) {
|
||||
encoding.Register()
|
||||
|
||||
app := tview.NewApplication()
|
||||
|
||||
title := fmt.Sprintf(
|
||||
"Chatting as %s via %s with %s via %s",
|
||||
cc.me, cc.endpoint, cc.user, cc.config.Endpoint,
|
||||
)
|
||||
|
||||
// Generate UI components.
|
||||
cc.mu.RLock()
|
||||
chatBox := NewChatBox(cc.palette, title)
|
||||
inputField := NewChatInput(cc.palette, cc.newMessageHandler(outCh))
|
||||
cc.mu.RUnlock()
|
||||
|
||||
// Layout the widgets in flex view.
|
||||
flex := tview.NewFlex().SetDirection(tview.FlexRow).
|
||||
AddItem(chatBox, 0, 1, false).
|
||||
AddItem(inputField, 3, 0, true)
|
||||
|
||||
go cc.updateChatBox(inCh, app, chatBox)
|
||||
|
||||
err := app.SetRoot(flex, true).SetFocus(inputField).EnableMouse(true).Run()
|
||||
config, err := Lookup(user)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
return Config{}, fmt.Errorf("error: failed to lookup user %s: %w", user, err)
|
||||
}
|
||||
|
||||
cli.cache[user] = config
|
||||
|
||||
return config, nil
|
||||
}
|
||||
|
||||
func (cli *Client) handleMessage(prehook, posthook string, msgs chan string) msgbus.HandlerFunc {
|
||||
return func(msg *msgbus.Message) error {
|
||||
if prehook != "" {
|
||||
out, err := exec.RunCmd(exec.DefaultRunCmdTimeout, prehook, bytes.NewBuffer(msg.Payload))
|
||||
if err != nil {
|
||||
log.WithError(err).Debugf("error running pre-hook %s", prehook)
|
||||
}
|
||||
log.Debugf("pre-hook: %q", out)
|
||||
}
|
||||
|
||||
data, _, err := salty.Decrypt(cli.key, msg.Payload)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "error decrypting message")
|
||||
return err
|
||||
}
|
||||
|
||||
msgs <- string(data)
|
||||
|
||||
if posthook != "" {
|
||||
out, err := exec.RunCmd(exec.DefaultRunCmdTimeout, posthook, bytes.NewBuffer(data))
|
||||
if err != nil {
|
||||
log.WithError(err).Debugf("error running post-hook %s", posthook)
|
||||
}
|
||||
log.Debugf("post-hook: %q", out)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// Open bi-directional stream between client and server.
|
||||
func (cc *chatClient) RunChat(inCh chan<- string, outCh <-chan string) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
func (cli *Client) Me() Addr { return cli.me }
|
||||
func (cli *Client) Endpoint() string { return cli.endpoint }
|
||||
|
||||
// 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)
|
||||
bus := msgbus_client.NewClient(uri, nil)
|
||||
|
||||
msgs := make(chan string)
|
||||
s := bus.Subscribe(inbox, cli.handleMessage(prehook, posthook, msgs))
|
||||
s.Start()
|
||||
|
||||
log.Infof("Connected to %s/%s", uri, inbox)
|
||||
|
||||
// Receives incoming messages on a separate goroutine to be non-blocking.
|
||||
go func() {
|
||||
for msg := range Read(ctx, cc.key, cc.endpoint, "", "") {
|
||||
inCh <- msg
|
||||
}
|
||||
<-ctx.Done()
|
||||
s.Stop()
|
||||
close(msgs)
|
||||
}()
|
||||
|
||||
// Forward/send outgoing messages.
|
||||
for msg := range outCh {
|
||||
if strings.TrimSpace(msg) == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
packedMsg := PackMessage(cc.me, msg)
|
||||
b, err := salty.Encrypt(cc.key, packedMsg, []string{cc.config.Key})
|
||||
if err != nil {
|
||||
log.Fatalf("error encrypting message: %v", err)
|
||||
}
|
||||
|
||||
err = Send(cc.config.Endpoint, string(b))
|
||||
if err != nil {
|
||||
log.Fatalf("error sending message: %v", err)
|
||||
}
|
||||
|
||||
inCh <- string(packedMsg)
|
||||
}
|
||||
return msgs
|
||||
}
|
||||
|
||||
// Send sends an encrypted message to the specified user
|
||||
func (cli *Client) Send(user, msg string) error {
|
||||
cfg, err := cli.getConfig(user)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error looking up user %s: %w", user, err)
|
||||
}
|
||||
|
||||
b, err := salty.Encrypt(cli.key, PackMessage(cli.me, msg), []string{cfg.Key})
|
||||
if err != nil {
|
||||
return fmt.Errorf("error encrypting message to %s: %w", user, err)
|
||||
}
|
||||
|
||||
if err := Send(cfg.Endpoint, string(b)); err != nil {
|
||||
return fmt.Errorf("error sending message to %s: %w", user, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
@ -9,6 +9,7 @@ import (
|
||||
"github.com/spf13/viper"
|
||||
|
||||
"go.mills.io/saltyim"
|
||||
"go.mills.io/saltyim/internal/tui"
|
||||
)
|
||||
|
||||
var chatCmd = &cobra.Command{
|
||||
@ -47,33 +48,27 @@ func init() {
|
||||
}
|
||||
|
||||
func chat(me saltyim.Addr, identity, endpoint, user string) {
|
||||
key, m, err := saltyim.GetIdentity(identity)
|
||||
cli, err := saltyim.NewClient(me, identity, endpoint)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "error opening identity: %q\n", identity)
|
||||
os.Exit(2)
|
||||
}
|
||||
if me.IsZero() {
|
||||
me = m
|
||||
}
|
||||
if me.IsZero() {
|
||||
fmt.Fprintf(os.Stderr, "unable to find your user addressn in %q\n", identity)
|
||||
fmt.Fprintln(os.Stderr, "tip: try adding # user: nick@domain to your identity")
|
||||
fmt.Fprintf(os.Stderr, "error initializing client: %s\n", err)
|
||||
os.Exit(2)
|
||||
}
|
||||
|
||||
// Set terminal title
|
||||
tui.SetTerminalTitle("Salty IM with %s", user)
|
||||
|
||||
// Initialize necessary channels.
|
||||
inCh := make(chan string)
|
||||
outCh := make(chan string)
|
||||
|
||||
// Initialize client.
|
||||
cc, err := saltyim.NewChatClient(key, me, endpoint, user)
|
||||
// Initialize ui.
|
||||
c, err := tui.NewChatTUI(cli, user)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "error creating chat: %s", err)
|
||||
fmt.Fprintf(os.Stderr, "error creating chat: %s\n", err)
|
||||
os.Exit(2)
|
||||
}
|
||||
|
||||
saltyim.SetTerminalTitle("Salty IM with %s", user)
|
||||
|
||||
go cc.RunChat(inCh, outCh)
|
||||
cc.SetScreen(inCh, outCh)
|
||||
// Run the ui loop
|
||||
go c.RunChat(inCh, outCh)
|
||||
c.SetScreen(inCh, outCh)
|
||||
}
|
||||
|
@ -40,16 +40,21 @@ func init() {
|
||||
func lookup(user string) {
|
||||
user = strings.TrimSpace(user)
|
||||
if user == "" {
|
||||
fmt.Fprintln(os.Stderr, "error: no user supplied")
|
||||
fmt.Fprintln(os.Stderr, "error no user supplied")
|
||||
os.Exit(2)
|
||||
}
|
||||
|
||||
config, err := saltyim.Lookup(user)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "error: failed to lookup user %q: %s", user, err)
|
||||
fmt.Fprintf(os.Stderr, "error looking up user %s: %s\n", user, err)
|
||||
os.Exit(2)
|
||||
}
|
||||
|
||||
data, err := json.Marshal(config)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "error parsing lookup response: %s\n", err)
|
||||
os.Exit(2)
|
||||
}
|
||||
|
||||
data, _ := json.Marshal(config)
|
||||
fmt.Fprintln(os.Stdout, string(data))
|
||||
}
|
||||
|
@ -10,6 +10,7 @@ import (
|
||||
"github.com/spf13/viper"
|
||||
|
||||
"go.mills.io/saltyim"
|
||||
"go.mills.io/saltyim/internal"
|
||||
)
|
||||
|
||||
const (
|
||||
@ -87,7 +88,7 @@ func makeuser(identity, endpoint, user string) {
|
||||
os.Exit(2)
|
||||
}
|
||||
|
||||
if saltyim.FileExists(identity) {
|
||||
if internal.FileExists(identity) {
|
||||
fmt.Fprintf(os.Stderr, "error identity %q already exists!\n", identity)
|
||||
}
|
||||
|
||||
@ -110,5 +111,5 @@ func makeuser(identity, endpoint, user string) {
|
||||
Addr: me,
|
||||
}
|
||||
|
||||
fmt.Println(saltyim.MustRenderString(postSetupInstructions, ctx))
|
||||
fmt.Println(internal.MustRenderString(postSetupInstructions, ctx))
|
||||
}
|
||||
|
@ -5,6 +5,7 @@ import (
|
||||
"log"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
@ -37,6 +38,12 @@ not specified defaults to the local user ($USER)`,
|
||||
}
|
||||
}
|
||||
|
||||
var me saltyim.Addr
|
||||
if sp := strings.Split(user, "@"); len(sp) > 1 {
|
||||
me.User = sp[0]
|
||||
me.Domain = sp[1]
|
||||
}
|
||||
|
||||
prehook, err := cmd.Flags().GetString("pre-hook")
|
||||
if err != nil {
|
||||
log.Fatal("error getting --pre-hook flag")
|
||||
@ -47,7 +54,7 @@ not specified defaults to the local user ($USER)`,
|
||||
log.Fatal("error getting --post-hook flag")
|
||||
}
|
||||
|
||||
read(endpoint, identity, prehook, posthook, args...)
|
||||
read(me, identity, endpoint, prehook, posthook, args...)
|
||||
},
|
||||
}
|
||||
|
||||
@ -71,10 +78,10 @@ func init() {
|
||||
)
|
||||
}
|
||||
|
||||
func read(endpoint, identity, prehook, posthook string, args ...string) {
|
||||
key, _, err := saltyim.GetIdentity(identity)
|
||||
func read(me saltyim.Addr, identity, endpoint string, prehook, posthook string, args ...string) {
|
||||
cli, err := saltyim.NewClient(me, identity, endpoint)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "error reading identity %q: %s", identity, err)
|
||||
fmt.Fprintf(os.Stderr, "error initializing client: %s\n", err)
|
||||
os.Exit(2)
|
||||
}
|
||||
|
||||
@ -89,7 +96,7 @@ func read(endpoint, identity, prehook, posthook string, args ...string) {
|
||||
cancel()
|
||||
}()
|
||||
|
||||
for msg := range saltyim.Read(ctx, key, endpoint, prehook, posthook) {
|
||||
for msg := range cli.Read(ctx, "", "", "") {
|
||||
fmt.Println(saltyim.FormatMessage(msg))
|
||||
}
|
||||
}
|
||||
|
@ -29,20 +29,6 @@ Getting Started:
|
||||
$ salty-chat make-user nick@domain
|
||||
# follow the instructions
|
||||
|
||||
Reading Messages:
|
||||
|
||||
$ salty-chat read
|
||||
$ salty-chat read -i inbox # if your nick != $USER
|
||||
|
||||
Sending Messages:
|
||||
|
||||
$ salty-chat send prologic@mills.io Hey
|
||||
$ echo "Hello World!" | salty-chat send prologic@mills.io # reads from stdin
|
||||
|
||||
Chatting:
|
||||
|
||||
$ salty-chat chat prologic@mills.io Hey
|
||||
|
||||
See https://salty.im for more details.`,
|
||||
PersistentPreRun: func(cmd *cobra.Command, args []string) {
|
||||
// set logging level
|
||||
|
@ -4,14 +4,12 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
|
||||
"go.mills.io/salty"
|
||||
"go.mills.io/saltyim"
|
||||
)
|
||||
|
||||
@ -37,12 +35,14 @@ 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
|
||||
}
|
||||
}
|
||||
@ -53,7 +53,7 @@ https://mills.io/.well-known/salty/prologic.json`,
|
||||
me.Domain = sp[1]
|
||||
}
|
||||
|
||||
send(me, identity, args[0], args[1:]...)
|
||||
send(me, identity, endpoint, args[0], args[1:]...)
|
||||
},
|
||||
}
|
||||
|
||||
@ -61,26 +61,16 @@ func init() {
|
||||
rootCmd.AddCommand(sendCmd)
|
||||
}
|
||||
|
||||
func send(me saltyim.Addr, identity, user string, args ...string) {
|
||||
func send(me saltyim.Addr, identity, endpoint, user string, args ...string) {
|
||||
user = strings.TrimSpace(user)
|
||||
if user == "" {
|
||||
fmt.Fprintf(os.Stderr, "error: no user supplied")
|
||||
os.Exit(2)
|
||||
}
|
||||
|
||||
config, err := saltyim.Lookup(user)
|
||||
cli, err := saltyim.NewClient(me, identity, endpoint)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "error: failed to lookup user %q: %s", user, err)
|
||||
os.Exit(2)
|
||||
}
|
||||
|
||||
key, me, err := saltyim.GetIdentity(identity)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "error opening identity: %q\n", identity)
|
||||
if me.IsZero() {
|
||||
fmt.Fprintf(os.Stderr, "unable to find your user addressn in %q\n", identity)
|
||||
fmt.Fprintln(os.Stderr, "tip: try adding # user: nick@domain to your identity")
|
||||
}
|
||||
fmt.Fprintf(os.Stderr, "error initializing client: %s\n", err)
|
||||
os.Exit(2)
|
||||
}
|
||||
|
||||
@ -97,13 +87,8 @@ func send(me saltyim.Addr, identity, user string, args ...string) {
|
||||
msg = strings.Join(args, " ")
|
||||
}
|
||||
|
||||
b, err := salty.Encrypt(key, saltyim.PackMessage(me, msg), []string{config.Key})
|
||||
if err != nil {
|
||||
log.Fatalf("error encrypting message: %v", err)
|
||||
}
|
||||
|
||||
err = saltyim.Send(config.Endpoint, string(b))
|
||||
if err != nil {
|
||||
log.Fatalf("error sending message: %v", err)
|
||||
if err := cli.Send(user, msg); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "error sending message to %s: %s", user, err)
|
||||
os.Exit(2)
|
||||
}
|
||||
}
|
||||
|
94
errors.go
94
errors.go
@ -1,94 +0,0 @@
|
||||
package saltyim
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
type ErrCommandKilled struct {
|
||||
Err error
|
||||
Signal syscall.Signal
|
||||
}
|
||||
|
||||
func (e *ErrCommandKilled) Is(target error) bool {
|
||||
if _, ok := target.(*ErrCommandKilled); ok {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (e *ErrCommandKilled) Error() string {
|
||||
return fmt.Sprintf("error: command killed: %s", e.Err)
|
||||
}
|
||||
|
||||
func (e *ErrCommandKilled) Unwrap() error {
|
||||
return e.Err
|
||||
}
|
||||
|
||||
type ErrCommandFailed struct {
|
||||
Err error
|
||||
Status int
|
||||
}
|
||||
|
||||
func (e *ErrCommandFailed) Is(target error) bool {
|
||||
if _, ok := target.(*ErrCommandFailed); ok {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (e *ErrCommandFailed) Error() string {
|
||||
return fmt.Sprintf("error: command failed: %s", e.Err)
|
||||
}
|
||||
|
||||
func (e *ErrCommandFailed) Unwrap() error {
|
||||
return e.Err
|
||||
}
|
||||
|
||||
type ErrTranscodeTimeout struct {
|
||||
Err error
|
||||
}
|
||||
|
||||
func (e *ErrTranscodeTimeout) Error() string {
|
||||
return fmt.Sprintf("error: transcode timed out: %s", e.Err)
|
||||
}
|
||||
|
||||
func (e *ErrTranscodeTimeout) Unwrap() error {
|
||||
return e.Err
|
||||
}
|
||||
|
||||
type ErrTranscodeFailed struct {
|
||||
Err error
|
||||
}
|
||||
|
||||
func (e *ErrTranscodeFailed) Error() string {
|
||||
return fmt.Sprintf("error: transcode failed: %s", e.Err)
|
||||
}
|
||||
|
||||
func (e *ErrTranscodeFailed) Unwrap() error {
|
||||
return e.Err
|
||||
}
|
||||
|
||||
type ErrAudioUploadFailed struct {
|
||||
Err error
|
||||
}
|
||||
|
||||
func (e *ErrAudioUploadFailed) Error() string {
|
||||
return fmt.Sprintf("error: audio upload failed: %s", e.Err)
|
||||
}
|
||||
|
||||
func (e *ErrAudioUploadFailed) Unwrap() error {
|
||||
return e.Err
|
||||
}
|
||||
|
||||
type ErrVideoUploadFailed struct {
|
||||
Err error
|
||||
}
|
||||
|
||||
func (e *ErrVideoUploadFailed) Error() string {
|
||||
return fmt.Sprintf("error: video upload failed: %s", e.Err)
|
||||
}
|
||||
|
||||
func (e *ErrVideoUploadFailed) Unwrap() error {
|
||||
return e.Err
|
||||
}
|
@ -11,6 +11,10 @@ import (
|
||||
"go.yarn.social/lextwt"
|
||||
)
|
||||
|
||||
const (
|
||||
DateTimeFormat = "2006-01-02 15:04:05"
|
||||
)
|
||||
|
||||
func boundedInt(value, low, high uint8) uint8 {
|
||||
diff := high - low
|
||||
return (((value - low) % diff) + low)
|
||||
@ -53,7 +57,7 @@ func FormatMessage(msg string) string {
|
||||
|
||||
return fmt.Sprintf(
|
||||
"%s\t%s\n%s\n",
|
||||
aurora.Sprintf(aurora.Blue(st.Timestamp.DateTime().Local().Format(dateTimeFormat))),
|
||||
aurora.Sprintf(aurora.Blue(st.Timestamp.DateTime().Local().Format(DateTimeFormat))),
|
||||
aurora.Sprintf(aurora.Index(userColor, st.User.String())),
|
||||
buf.String(),
|
||||
)
|
||||
|
2
go.mod
2
go.mod
@ -11,7 +11,7 @@ require (
|
||||
)
|
||||
|
||||
require (
|
||||
git.mills.io/prologic/msgbus v0.1.6
|
||||
git.mills.io/prologic/msgbus v0.1.7
|
||||
github.com/dim13/crc24 v0.0.0-20190831075008-15d874f06514
|
||||
github.com/gdamore/tcell/v2 v2.4.1-0.20210905002822-f057f0a857a1
|
||||
github.com/google/go-cmp v0.5.7 // indirect
|
||||
|
4
go.sum
4
go.sum
@ -48,8 +48,8 @@ cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RX
|
||||
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
|
||||
cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo=
|
||||
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
||||
git.mills.io/prologic/msgbus v0.1.6 h1:ayEmGsuvzfatQ2j5HwYRtH78BjaFScRjXhlQ7SmYQ30=
|
||||
git.mills.io/prologic/msgbus v0.1.6/go.mod h1:1fzdbZsp32pQfMD2ULeozCwNRZ1qx8XTjFbN/M+QGSs=
|
||||
git.mills.io/prologic/msgbus v0.1.7 h1:UfqB9J3NMrlhjW0gHHqCMwisOP1hAz/rBVKADYF2Wzg=
|
||||
git.mills.io/prologic/msgbus v0.1.7/go.mod h1:1fzdbZsp32pQfMD2ULeozCwNRZ1qx8XTjFbN/M+QGSs=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||
github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ=
|
||||
|
3
internal/doc.go
Normal file
3
internal/doc.go
Normal file
@ -0,0 +1,3 @@
|
||||
package internal
|
||||
|
||||
/* internal */
|
47
internal/exec/cmd.go
Normal file
47
internal/exec/cmd.go
Normal file
@ -0,0 +1,47 @@
|
||||
package exec
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"os/exec"
|
||||
"syscall"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
DefaultRunCmdTimeout = time.Second * 3
|
||||
)
|
||||
|
||||
// RunCmd executes the given command and arguments and stdin and ensures the
|
||||
// command takes no longer than the timeout before the command is terminated.
|
||||
func RunCmd(timeout time.Duration, command string, stdin io.Reader, args ...string) (string, error) {
|
||||
var (
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
)
|
||||
|
||||
if timeout > 0 {
|
||||
ctx, cancel = context.WithTimeout(context.Background(), timeout)
|
||||
} else {
|
||||
ctx, cancel = context.WithCancel(context.Background())
|
||||
}
|
||||
defer cancel()
|
||||
|
||||
cmd := exec.CommandContext(ctx, command, args...)
|
||||
cmd.Stdin = stdin
|
||||
|
||||
out, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
if exitError, ok := err.(*exec.ExitError); ok {
|
||||
if ws, ok := exitError.Sys().(syscall.WaitStatus); ok && ws.Signal() == syscall.SIGKILL {
|
||||
err = &ErrCommandKilled{Err: err, Signal: ws.Signal()}
|
||||
} else {
|
||||
err = &ErrCommandFailed{Err: err, Status: exitError.ExitCode()}
|
||||
}
|
||||
}
|
||||
|
||||
return "", err
|
||||
}
|
||||
|
||||
return string(out), nil
|
||||
}
|
46
internal/exec/errors.go
Normal file
46
internal/exec/errors.go
Normal file
@ -0,0 +1,46 @@
|
||||
package exec
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
type ErrCommandKilled struct {
|
||||
Err error
|
||||
Signal syscall.Signal
|
||||
}
|
||||
|
||||
func (e *ErrCommandKilled) Is(target error) bool {
|
||||
if _, ok := target.(*ErrCommandKilled); ok {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (e *ErrCommandKilled) Error() string {
|
||||
return fmt.Sprintf("error: command killed: %s", e.Err)
|
||||
}
|
||||
|
||||
func (e *ErrCommandKilled) Unwrap() error {
|
||||
return e.Err
|
||||
}
|
||||
|
||||
type ErrCommandFailed struct {
|
||||
Err error
|
||||
Status int
|
||||
}
|
||||
|
||||
func (e *ErrCommandFailed) Is(target error) bool {
|
||||
if _, ok := target.(*ErrCommandFailed); ok {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (e *ErrCommandFailed) Error() string {
|
||||
return fmt.Sprintf("error: command failed: %s", e.Err)
|
||||
}
|
||||
|
||||
func (e *ErrCommandFailed) Unwrap() error {
|
||||
return e.Err
|
||||
}
|
180
internal/tui/tui.go
Normal file
180
internal/tui/tui.go
Normal file
@ -0,0 +1,180 @@
|
||||
package tui
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"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"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"go.yarn.social/lextwt"
|
||||
|
||||
"go.mills.io/saltyim"
|
||||
)
|
||||
|
||||
func getUserColor(s string) tcell.Color {
|
||||
c := crc24.Sum([]byte(s))
|
||||
return tcell.NewRGBColor(
|
||||
int32(c>>0x10),
|
||||
int32(c>>0x08),
|
||||
int32(c),
|
||||
)
|
||||
}
|
||||
|
||||
type ChatTUI struct {
|
||||
mu sync.RWMutex
|
||||
|
||||
cli *saltyim.Client
|
||||
me saltyim.Addr
|
||||
user string
|
||||
config saltyim.Config
|
||||
|
||||
// Configurations.
|
||||
palette map[string]string
|
||||
}
|
||||
|
||||
// NewChatTUI initializes a new chatApp.
|
||||
// Sets up connection with broker, and initializes UI.
|
||||
func NewChatTUI(cli *saltyim.Client, user string) (*ChatTUI, error) {
|
||||
config, err := saltyim.Lookup(user)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error looking up user %s: %s", user, err)
|
||||
}
|
||||
|
||||
return &ChatTUI{
|
||||
cli: cli,
|
||||
user: user,
|
||||
config: config,
|
||||
|
||||
palette: map[string]string{
|
||||
"background": "000000",
|
||||
"border": "4C5C68",
|
||||
"title": "46494C",
|
||||
"date": "E76F51",
|
||||
"text": "577399",
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// newMessageHandler returns an InputHandler that handles outgoing messages.
|
||||
func (c *ChatTUI) newMessageHandler(outCh chan<- string) InputHandler {
|
||||
handler := func(message string) {
|
||||
c.mu.RLock()
|
||||
outCh <- message
|
||||
c.mu.RUnlock()
|
||||
}
|
||||
|
||||
return handler
|
||||
}
|
||||
|
||||
// updateChatBox updates chatBox component with incoming messages.
|
||||
func (c *ChatTUI) updateChatBox(inCh <-chan string, app *tview.Application,
|
||||
chatBox *tview.TextView) {
|
||||
|
||||
for in := range inCh {
|
||||
s, err := lextwt.ParseSalty(in)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
switch s := s.(type) {
|
||||
|
||||
case *lextwt.SaltyEvent:
|
||||
// Ignored for now
|
||||
|
||||
case *lextwt.SaltyText:
|
||||
if s.User.String() != c.cli.Me().String() && s.User.String() != c.user {
|
||||
continue
|
||||
}
|
||||
|
||||
buf := &bytes.Buffer{}
|
||||
_, _, width, _ := chatBox.GetInnerRect()
|
||||
f := formatter.Formatter{
|
||||
Writer: buf,
|
||||
Indent: []byte("> "),
|
||||
Width: width - 1,
|
||||
}
|
||||
|
||||
if _, err := f.Write([]byte(s.LiteralText())); err != nil {
|
||||
log.WithError(err).Error("error formatting message")
|
||||
continue
|
||||
}
|
||||
|
||||
c.mu.RLock()
|
||||
app.QueueUpdateDraw(func() {
|
||||
fmt.Fprintf(chatBox,
|
||||
"[#%s]%s [#%x]%s\n[#%s]%s\n\n",
|
||||
c.palette["date"],
|
||||
s.Timestamp.DateTime().Local().Format(saltyim.DateTimeFormat),
|
||||
getUserColor(s.User.String()).Hex(),
|
||||
s.User.String(),
|
||||
c.palette["text"],
|
||||
buf.String(),
|
||||
)
|
||||
})
|
||||
c.mu.RUnlock()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// setScreen initializes the layout and UI components.
|
||||
func (c *ChatTUI) SetScreen(inCh <-chan string, outCh chan<- string) {
|
||||
encoding.Register()
|
||||
|
||||
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,
|
||||
)
|
||||
|
||||
// Generate UI components.
|
||||
c.mu.RLock()
|
||||
chatBox := NewChatBox(c.palette, title)
|
||||
inputField := NewChatInput(c.palette, c.newMessageHandler(outCh))
|
||||
c.mu.RUnlock()
|
||||
|
||||
// Layout the widgets in flex view.
|
||||
flex := tview.NewFlex().SetDirection(tview.FlexRow).
|
||||
AddItem(chatBox, 0, 1, false).
|
||||
AddItem(inputField, 3, 0, true)
|
||||
|
||||
go c.updateChatBox(inCh, app, chatBox)
|
||||
|
||||
err := app.SetRoot(flex, true).SetFocus(inputField).EnableMouse(true).Run()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
// Open bi-directional stream between client and server.
|
||||
func (c *ChatTUI) RunChat(inCh chan<- string, outCh <-chan string) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
// Receives incoming messages on a separate goroutine to be non-blocking.
|
||||
go func() {
|
||||
for msg := range c.cli.Read(ctx, "", "", "") {
|
||||
inCh <- msg
|
||||
}
|
||||
}()
|
||||
|
||||
// Forward/send outgoing messages.
|
||||
for msg := range outCh {
|
||||
if strings.TrimSpace(msg) == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
if err := c.cli.Send(c.user, msg); err != nil {
|
||||
log.WithError(err).Fatal("error sending message")
|
||||
}
|
||||
|
||||
inCh <- string(saltyim.PackMessage(c.cli.Me(), msg))
|
||||
}
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
package saltyim
|
||||
package tui
|
||||
|
||||
import (
|
||||
"regexp"
|
10
internal/tui/utils.go
Normal file
10
internal/tui/utils.go
Normal file
@ -0,0 +1,10 @@
|
||||
package tui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// SetTerminalTitle sets the Terminal/Console's title
|
||||
func SetTerminalTitle(format string, args ...interface{}) {
|
||||
fmt.Printf("\033]0;%s\007", fmt.Sprintf(format, args...))
|
||||
}
|
120
internal/utils.go
Normal file
120
internal/utils.go
Normal file
@ -0,0 +1,120 @@
|
||||
package internal
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"text/template"
|
||||
"time"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"go.mills.io/saltyim"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultRequestTimeout = time.Second * 5
|
||||
)
|
||||
|
||||
func MustRenderString(s string, ctx interface{}) string {
|
||||
out, err := RenderString(s, ctx)
|
||||
if err != nil {
|
||||
log.WithError(err).Fatal("error rendering string template")
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// DirExists returns true if the given directory exists
|
||||
func DirExists(name string) bool {
|
||||
stat, err := os.Stat(name)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return false
|
||||
}
|
||||
return false
|
||||
}
|
||||
return stat.IsDir()
|
||||
}
|
||||
|
||||
// FileExists returns true if the given file exists
|
||||
func FileExists(name string) bool {
|
||||
if _, err := os.Stat(name); err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// RenderString renders the string `s` as a `text/template` using the provided
|
||||
// context `ctx` as input into the template.
|
||||
// Typically used to render the results into a Markdown document.
|
||||
func RenderString(s string, ctx interface{}) (string, error) {
|
||||
t := template.Must(template.New("s").Parse(s))
|
||||
buf := bytes.NewBuffer([]byte{})
|
||||
err := t.Execute(buf, ctx)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return buf.String(), nil
|
||||
}
|
||||
|
||||
// Request is a generic request handling function for making artbitrary HTPT
|
||||
// requests to Salty endpoints for looking up Salty Addresses, Configs and
|
||||
// publishing encrypted messages.
|
||||
func Request(method, uri string, headers http.Header, body io.Reader) (*http.Response, error) {
|
||||
req, err := http.NewRequest(method, uri, body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s: http.NewRequest fail: %s", uri, err)
|
||||
}
|
||||
|
||||
if headers == nil {
|
||||
headers = make(http.Header)
|
||||
}
|
||||
|
||||
// Set a default User-Agent (if none set)
|
||||
if headers.Get("User-Agent") == "" {
|
||||
headers.Set("User-Agent", fmt.Sprintf("saltyim/%s", saltyim.FullVersion()))
|
||||
}
|
||||
|
||||
req.Header = headers
|
||||
|
||||
client := http.Client{
|
||||
Timeout: defaultRequestTimeout,
|
||||
}
|
||||
|
||||
res, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s: client.Do fail: %s", uri, err)
|
||||
}
|
||||
|
||||
if res.StatusCode/100 != 2 {
|
||||
return nil, fmt.Errorf("non-2xx response received: %s", res.Status)
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// CmdExists ...
|
||||
func CmdExists(cmd string) bool {
|
||||
_, err := exec.LookPath(cmd)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// SetTerminalTitle sets the Terminal/Console's title
|
||||
func SetTerminalTitle(format string, args ...interface{}) {
|
||||
fmt.Printf("\033]0;%s\007", fmt.Sprintf(format, args...))
|
||||
}
|
||||
|
||||
func SplitInbox(endpoint string) (string, string) {
|
||||
idx := strings.LastIndex(endpoint, "/")
|
||||
if idx == -1 {
|
||||
return "", ""
|
||||
}
|
||||
return endpoint[:idx], endpoint[idx+1:]
|
||||
}
|
70
readmsgs.go
70
readmsgs.go
@ -1,70 +0,0 @@
|
||||
package saltyim
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"git.mills.io/prologic/msgbus"
|
||||
"git.mills.io/prologic/msgbus/client"
|
||||
"github.com/keys-pub/keys"
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"go.mills.io/salty"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultRunCmdTimeout = time.Second * 3
|
||||
)
|
||||
|
||||
func handleMessage(key *keys.EdX25519Key, prehook, posthook string, msgs chan string) msgbus.HandlerFunc {
|
||||
return func(msg *msgbus.Message) error {
|
||||
if prehook != "" {
|
||||
out, err := RunCmd(defaultRunCmdTimeout, prehook, bytes.NewBuffer(msg.Payload))
|
||||
if err != nil {
|
||||
log.WithError(err).Debugf("error running pre-hook %s", prehook)
|
||||
}
|
||||
log.Debugf("pre-hook: %q", out)
|
||||
}
|
||||
|
||||
data, _, err := salty.Decrypt(key, msg.Payload)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "error decrypting message")
|
||||
return err
|
||||
}
|
||||
|
||||
msgs <- string(data)
|
||||
|
||||
if posthook != "" {
|
||||
out, err := RunCmd(defaultRunCmdTimeout, posthook, bytes.NewBuffer(data))
|
||||
if err != nil {
|
||||
log.WithError(err).Debugf("error running post-hook %s", posthook)
|
||||
}
|
||||
log.Debugf("post-hook: %q", out)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// Read ...
|
||||
func Read(ctx context.Context, key *keys.EdX25519Key, endpoint, prehook, posthook string) chan string {
|
||||
uri, inbox := SplitInbox(endpoint)
|
||||
client := client.NewClient(uri, nil)
|
||||
|
||||
msgs := make(chan string)
|
||||
s := client.Subscribe(inbox, handleMessage(key, prehook, posthook, msgs))
|
||||
s.Start()
|
||||
|
||||
log.Infof("Connected to %s/%s", uri, inbox)
|
||||
|
||||
go func() {
|
||||
<-ctx.Done()
|
||||
s.Stop()
|
||||
close(msgs)
|
||||
}()
|
||||
|
||||
return msgs
|
||||
}
|
28
sendmsg.go
28
sendmsg.go
@ -1,28 +0,0 @@
|
||||
package saltyim
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// Packmessage formts an outoing message in the Message Format
|
||||
// <timestamp>\t(<sender>) <message>
|
||||
func PackMessage(me Addr, msg string) []byte {
|
||||
return []byte(fmt.Sprint(time.Now().UTC().Format(time.RFC3339), "\t", me.Formatted(), "\t", msg))
|
||||
}
|
||||
|
||||
// Send sends the encrypted message `msg` to the Endpoint `endpoint` using a
|
||||
// `POST` request and returns nil on success or an error on failure.
|
||||
func Send(endpoint, msg string) error {
|
||||
res, err := Request(http.MethodPost, endpoint, nil, bytes.NewBufferString(msg))
|
||||
if err != nil {
|
||||
log.WithError(err).Errorf("error publishing message to %s", endpoint)
|
||||
return err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
return nil
|
||||
}
|
98
utils.go
98
utils.go
@ -1,69 +1,17 @@
|
||||
package saltyim
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"syscall"
|
||||
"text/template"
|
||||
"time"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultRequestTimeout = time.Second * 5
|
||||
)
|
||||
|
||||
func MustRenderString(s string, ctx interface{}) string {
|
||||
out, err := RenderString(s, ctx)
|
||||
if err != nil {
|
||||
log.WithError(err).Fatal("error rendering string template")
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// DirExists returns true if the given directory exists
|
||||
func DirExists(name string) bool {
|
||||
stat, err := os.Stat(name)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return false
|
||||
}
|
||||
return false
|
||||
}
|
||||
return stat.IsDir()
|
||||
}
|
||||
|
||||
// FileExists returns true if the given file exists
|
||||
func FileExists(name string) bool {
|
||||
if _, err := os.Stat(name); err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// RenderString renders the string `s` as a `text/template` using the provided
|
||||
// context `ctx` as input into the template.
|
||||
// Typically used to render the results into a Markdown document.
|
||||
func RenderString(s string, ctx interface{}) (string, error) {
|
||||
t := template.Must(template.New("s").Parse(s))
|
||||
buf := bytes.NewBuffer([]byte{})
|
||||
err := t.Execute(buf, ctx)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return buf.String(), nil
|
||||
}
|
||||
|
||||
// Request is a generic request handling function for making artbitrary HTPT
|
||||
// requests to Salty endpoints for looking up Salty Addresses, Configs and
|
||||
// publishing encrypted messages.
|
||||
@ -100,50 +48,8 @@ func Request(method, uri string, headers http.Header, body io.Reader) (*http.Res
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// CmdExists ...
|
||||
func CmdExists(cmd string) bool {
|
||||
_, err := exec.LookPath(cmd)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// RunCmd ...
|
||||
func RunCmd(timeout time.Duration, command string, stdin io.Reader, args ...string) (string, error) {
|
||||
var (
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
)
|
||||
|
||||
if timeout > 0 {
|
||||
ctx, cancel = context.WithTimeout(context.Background(), timeout)
|
||||
} else {
|
||||
ctx, cancel = context.WithCancel(context.Background())
|
||||
}
|
||||
defer cancel()
|
||||
|
||||
cmd := exec.CommandContext(ctx, command, args...)
|
||||
cmd.Stdin = stdin
|
||||
|
||||
out, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
if exitError, ok := err.(*exec.ExitError); ok {
|
||||
if ws, ok := exitError.Sys().(syscall.WaitStatus); ok && ws.Signal() == syscall.SIGKILL {
|
||||
err = &ErrCommandKilled{Err: err, Signal: ws.Signal()}
|
||||
} else {
|
||||
err = &ErrCommandFailed{Err: err, Status: exitError.ExitCode()}
|
||||
}
|
||||
}
|
||||
|
||||
return "", err
|
||||
}
|
||||
|
||||
return string(out), nil
|
||||
}
|
||||
|
||||
// SetTerminalTitle sets the Terminal/Console's title
|
||||
func SetTerminalTitle(format string, args ...interface{}) {
|
||||
fmt.Printf("\033]0;%s\007", fmt.Sprintf(format, args...))
|
||||
}
|
||||
|
||||
// SplitInbox splits and endpoint into it's components (inbox, uri)
|
||||
// where inbox is a topic queue on the Salty broker uri
|
||||
func SplitInbox(endpoint string) (string, string) {
|
||||
idx := strings.LastIndex(endpoint, "/")
|
||||
if idx == -1 {
|
||||
|
Loading…
Reference in New Issue
Block a user