Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ You will be prompted for your FMSG address (e.g. `@user@example.com`). A JWT tok
|---------|-------------|
| `fmsg login` | Authenticate and store a local token |
| `fmsg list [--limit N] [--offset N]` | List messages for the authenticated user |
| `fmsg wait [--since-id N] [--timeout N]` | Long-poll for new messages |
| `fmsg get <message-id>` | Retrieve a message by ID |
| `fmsg send <recipient> <file\|text\|->` | Send a message (file path, text, or `-` for stdin) |
| `fmsg update <message-id> [file\|text\|->` | Update a draft message |
Expand All @@ -57,6 +58,10 @@ fmsg login
fmsg list
fmsg list --limit 10 --offset 20

# Wait for a new message
fmsg wait
fmsg wait --since-id 120 --timeout 10

# Get a specific message
fmsg get 101

Expand Down
3 changes: 3 additions & 0 deletions cmd/get.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ var getCmd = &cobra.Command{
addTo, _ := json.Marshal(msg.AddTo)
fmt.Printf("Add-To: %s\n", string(addTo))
}
if msg.AddToFrom != nil {
fmt.Printf("Add-To-From: %s\n", *msg.AddToFrom)
}
if msg.PID != nil {
fmt.Printf("PID: %d\n", *msg.PID)
}
Expand Down
58 changes: 58 additions & 0 deletions cmd/wait.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package cmd

import (
"fmt"
"os"

"github.com/markmnl/fmsg-cli/internal/api"
"github.com/markmnl/fmsg-cli/internal/auth"
"github.com/markmnl/fmsg-cli/internal/config"
"github.com/spf13/cobra"
)

var (
waitSinceID int64
waitTimeout int
)

var waitCmd = &cobra.Command{
Use: "wait",
Short: "Long-poll for new messages",
Long: `Long-poll until a new message arrives for the authenticated user.

Returns immediately when a new message is available, or exits after timeout when none arrive.`,
RunE: func(cmd *cobra.Command, args []string) error {
if waitSinceID < 0 {
return fmt.Errorf("--since-id must be >= 0")
}
if waitTimeout < 1 || waitTimeout > 60 {
return fmt.Errorf("--timeout must be between 1 and 60 seconds")
}

creds, err := auth.LoadValid()
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}

client := api.New(config.GetAPIURL(), creds.Token)
result, err := client.WaitForMessage(waitSinceID, waitTimeout)
if err != nil {
return err
}

if result.HasNew {
fmt.Printf("New message available. Latest ID: %d\n", result.LatestID)
return nil
}

fmt.Println("No new messages.")
return nil
},
}

func init() {
waitCmd.Flags().Int64Var(&waitSinceID, "since-id", 0, "Only consider messages with ID greater than this value")
waitCmd.Flags().IntVar(&waitTimeout, "timeout", 25, "Maximum seconds to wait (1-60)")
rootCmd.AddCommand(waitCmd)
}
47 changes: 47 additions & 0 deletions internal/api/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ type MessageListItem struct {
From string `json:"from"`
To []string `json:"to"`
AddTo []string `json:"add_to"`
AddToFrom *string `json:"add_to_from"`
Time *float64 `json:"time"`
Topic string `json:"topic"`
Type string `json:"type"`
Expand All @@ -102,6 +103,7 @@ type Message struct {
From string `json:"from"`
To []string `json:"to"`
AddTo []string `json:"add_to"`
AddToFrom *string `json:"add_to_from"`
Time *float64 `json:"time"`
Topic string `json:"topic"`
Type string `json:"type"`
Expand All @@ -126,6 +128,12 @@ type AddRecipientsResponse struct {
Added int `json:"added"`
}

// WaitResponse is the response from GET /fmsg/wait.
type WaitResponse struct {
HasNew bool `json:"has_new"`
LatestID int64 `json:"latest_id"`
}

// ListMessages returns messages for the authenticated user.
func (c *Client) ListMessages(limit, offset int) ([]MessageListItem, error) {
u, err := url.Parse(c.BaseURL + "/fmsg")
Expand Down Expand Up @@ -163,6 +171,45 @@ func (c *Client) ListMessages(limit, offset int) ([]MessageListItem, error) {
return messages, nil
}

// WaitForMessage long-polls for a new message for the authenticated user.
func (c *Client) WaitForMessage(sinceID int64, timeout int) (*WaitResponse, error) {
u, err := url.Parse(c.BaseURL + "/fmsg/wait")
if err != nil {
return nil, err
}

q := u.Query()
q.Set("since_id", strconv.FormatInt(sinceID, 10))
q.Set("timeout", strconv.Itoa(timeout))
u.RawQuery = q.Encode()

req, err := http.NewRequest(http.MethodGet, u.String(), nil)
if err != nil {
return nil, err
}

resp, err := c.do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()

if err := checkStatus(resp); err != nil {
return nil, err
}

if resp.StatusCode == http.StatusNoContent {
return &WaitResponse{HasNew: false}, nil
}

var result WaitResponse
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return nil, fmt.Errorf("decoding response: %w", err)
}

return &result, nil
}

// GetMessage retrieves a single message by ID.
func (c *Client) GetMessage(id string) (*Message, error) {
req, err := http.NewRequest(http.MethodGet, c.BaseURL+"/fmsg/"+id, nil)
Expand Down