From ee9971322d12905e8e0f02acb69434ac0b39a9fe Mon Sep 17 00:00:00 2001 From: Mark Mennell Date: Sat, 11 Apr 2026 17:08:49 +0800 Subject: [PATCH] wait cmd --- README.md | 5 ++++ cmd/get.go | 3 +++ cmd/wait.go | 58 ++++++++++++++++++++++++++++++++++++++++++ internal/api/client.go | 47 ++++++++++++++++++++++++++++++++++ 4 files changed, 113 insertions(+) create mode 100644 cmd/wait.go diff --git a/README.md b/README.md index b240c5f..daf76c3 100644 --- a/README.md +++ b/README.md @@ -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 ` | Retrieve a message by ID | | `fmsg send ` | Send a message (file path, text, or `-` for stdin) | | `fmsg update [file\|text\|->` | Update a draft message | @@ -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 diff --git a/cmd/get.go b/cmd/get.go index e8dfae3..7c8f960 100644 --- a/cmd/get.go +++ b/cmd/get.go @@ -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) } diff --git a/cmd/wait.go b/cmd/wait.go new file mode 100644 index 0000000..8409949 --- /dev/null +++ b/cmd/wait.go @@ -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) +} diff --git a/internal/api/client.go b/internal/api/client.go index 5304f1a..a8f2884 100644 --- a/internal/api/client.go +++ b/internal/api/client.go @@ -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"` @@ -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"` @@ -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") @@ -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)