feat: add message list filters (#153) (thanks @draix)

This commit is contained in:
Peter Steinberger 2026-04-21 04:53:27 +01:00
parent 372e1fc257
commit dffcda4481
No known key found for this signature in database
5 changed files with 117 additions and 12 deletions

View File

@ -5,6 +5,7 @@
### Added
- CLI: add `--full` to disable table truncation; piped output now keeps full message IDs. (#13 — thanks @rickhallett)
- Messages: add `messages list --sender`, `--from-me`, `--from-them`, and `--asc` filters. (#153 — thanks @draix)
### Security

View File

@ -25,9 +25,13 @@ func newMessagesCmd(flags *rootFlags) *cobra.Command {
func newMessagesListCmd(flags *rootFlags) *cobra.Command {
var chat string
var sender string
var limit int
var afterStr string
var beforeStr string
var fromMe bool
var fromThem bool
var asc bool
cmd := &cobra.Command{
Use: "list",
@ -36,6 +40,10 @@ func newMessagesListCmd(flags *rootFlags) *cobra.Command {
ctx, cancel := withTimeout(context.Background(), flags)
defer cancel()
if fromMe && fromThem {
return fmt.Errorf("--from-me and --from-them are mutually exclusive")
}
a, lk, err := newApp(ctx, flags, false, false)
if err != nil {
return err
@ -59,11 +67,24 @@ func newMessagesListCmd(flags *rootFlags) *cobra.Command {
before = &t
}
var fromMeFilter *bool
switch {
case fromMe:
v := true
fromMeFilter = &v
case fromThem:
v := false
fromMeFilter = &v
}
msgs, err := a.DB().ListMessages(store.ListMessagesParams{
ChatJID: chat,
Limit: limit,
After: after,
Before: before,
ChatJID: chat,
SenderJID: sender,
Limit: limit,
After: after,
Before: before,
FromMe: fromMeFilter,
Asc: asc,
})
if err != nil {
return err
@ -80,10 +101,14 @@ func newMessagesListCmd(flags *rootFlags) *cobra.Command {
},
}
cmd.Flags().StringVar(&chat, "chat", "", "chat JID")
cmd.Flags().IntVar(&limit, "limit", 50, "limit results")
cmd.Flags().StringVar(&chat, "chat", "", "filter by chat JID")
cmd.Flags().StringVar(&sender, "sender", "", "filter by sender JID")
cmd.Flags().IntVar(&limit, "limit", 50, "max number of messages to return")
cmd.Flags().StringVar(&afterStr, "after", "", "only messages after time (RFC3339 or YYYY-MM-DD)")
cmd.Flags().StringVar(&beforeStr, "before", "", "only messages before time (RFC3339 or YYYY-MM-DD)")
cmd.Flags().BoolVar(&fromMe, "from-me", false, "only messages sent by me")
cmd.Flags().BoolVar(&fromThem, "from-them", false, "only messages received (not sent by me)")
cmd.Flags().BoolVar(&asc, "asc", false, "show oldest messages first (default: newest first)")
return cmd
}

View File

@ -165,7 +165,7 @@ WhatsApp Web history is best-effort. If you want to try fetching *older* message
### Messages
- `wacli messages list [--chat JID] [--limit N] [--before TS] [--after TS]`
- `wacli messages list [--chat JID] [--sender JID] [--from-me|--from-them] [--asc] [--limit N] [--before TS] [--after TS]`
- `wacli messages search <query> [--chat JID] [--from JID] [--limit N] [--before TS] [--after TS] [--type text|image|video|audio|document]`
- `wacli messages show --chat JID --id MSG_ID`
- `wacli messages context --chat JID --id MSG_ID [--before N] [--after N]`

View File

@ -59,10 +59,13 @@ func (d *DB) UpsertMessage(p UpsertMessageParams) error {
}
type ListMessagesParams struct {
ChatJID string
Limit int
Before *time.Time
After *time.Time
ChatJID string
SenderJID string
Limit int
Before *time.Time
After *time.Time
FromMe *bool
Asc bool
}
func (d *DB) ListMessages(p ListMessagesParams) ([]Message, error) {
@ -87,7 +90,19 @@ func (d *DB) ListMessages(p ListMessagesParams) ([]Message, error) {
query += " AND m.ts < ?"
args = append(args, unix(*p.Before))
}
query += " ORDER BY m.ts DESC LIMIT ?"
if strings.TrimSpace(p.SenderJID) != "" {
query += " AND m.sender_jid = ?"
args = append(args, strings.TrimSpace(p.SenderJID))
}
if p.FromMe != nil {
query += " AND m.from_me = ?"
args = append(args, boolToInt(*p.FromMe))
}
if p.Asc {
query += " ORDER BY m.ts ASC LIMIT ?"
} else {
query += " ORDER BY m.ts DESC LIMIT ?"
}
args = append(args, p.Limit)
return d.scanMessages(query, args...)
}

View File

@ -3,6 +3,7 @@ package store
import (
"database/sql"
"path/filepath"
"strings"
"testing"
"time"
)
@ -128,6 +129,69 @@ func TestMessageUpsertIdempotentAndContext(t *testing.T) {
}
}
func TestListMessagesFiltersAndOrdering(t *testing.T) {
db := openTestDB(t)
chat := "chat@s.whatsapp.net"
otherChat := "other@s.whatsapp.net"
base := time.Date(2024, 2, 1, 0, 0, 0, 0, time.UTC)
for _, jid := range []string{chat, otherChat} {
if err := db.UpsertChat(jid, "dm", jid, base); err != nil {
t.Fatalf("UpsertChat %s: %v", jid, err)
}
}
rows := []UpsertMessageParams{
{ChatJID: chat, MsgID: "old-from-alice", SenderJID: "alice@s.whatsapp.net", Timestamp: base, Text: "old"},
{ChatJID: chat, MsgID: "new-from-me", SenderJID: "me@s.whatsapp.net", Timestamp: base.Add(time.Second), FromMe: true, Text: "new"},
{ChatJID: otherChat, MsgID: "other-chat", SenderJID: "alice@s.whatsapp.net", Timestamp: base.Add(2 * time.Second), Text: "other"},
}
for _, row := range rows {
if err := db.UpsertMessage(row); err != nil {
t.Fatalf("UpsertMessage %s: %v", row.MsgID, err)
}
}
msgs, err := db.ListMessages(ListMessagesParams{ChatJID: chat, Limit: 10})
if err != nil {
t.Fatalf("ListMessages: %v", err)
}
if got := messageIDs(msgs); got != "new-from-me,old-from-alice" {
t.Fatalf("default order = %s", got)
}
msgs, err = db.ListMessages(ListMessagesParams{ChatJID: chat, Limit: 10, Asc: true})
if err != nil {
t.Fatalf("ListMessages asc: %v", err)
}
if got := messageIDs(msgs); got != "old-from-alice,new-from-me" {
t.Fatalf("asc order = %s", got)
}
fromMe := true
msgs, err = db.ListMessages(ListMessagesParams{ChatJID: chat, Limit: 10, FromMe: &fromMe})
if err != nil {
t.Fatalf("ListMessages fromMe: %v", err)
}
if got := messageIDs(msgs); got != "new-from-me" {
t.Fatalf("fromMe filter = %s", got)
}
msgs, err = db.ListMessages(ListMessagesParams{ChatJID: chat, SenderJID: "alice@s.whatsapp.net", Limit: 10})
if err != nil {
t.Fatalf("ListMessages sender: %v", err)
}
if got := messageIDs(msgs); got != "old-from-alice" {
t.Fatalf("sender filter = %s", got)
}
}
func messageIDs(msgs []Message) string {
out := make([]string, 0, len(msgs))
for _, msg := range msgs {
out = append(out, msg.MsgID)
}
return strings.Join(out, ",")
}
func TestMediaDownloadInfoAndMarkDownloaded(t *testing.T) {
db := openTestDB(t)