feat: expose forwarded message metadata
This commit is contained in:
parent
fca5b96138
commit
7533e4bef9
@ -26,6 +26,7 @@
|
||||
- Groups: hide groups after `groups leave`, mark missing joined groups as left during refresh, and show them again if a later refresh reports membership. (#125, #129 — thanks @SeifBenayed and @ImLukeF)
|
||||
- History: cap on-demand backfill at 500 messages per request and 100 requests per run.
|
||||
- Messages: normalize device-specific `@s.whatsapp.net` JIDs before storing chats, contacts, and senders.
|
||||
- Messages: store forwarded-message metadata and add `--forwarded` filters for list/search. (#24 — thanks @bnvyas)
|
||||
- Doctor: report lock owner PID and distinguish paired stores locked by another process. (#105 — thanks @artemgetmann)
|
||||
- Media: recover panics per download job so one bad payload no longer drains the worker pool. (#179 — thanks @shaun0927)
|
||||
- Messages: attribute history messages from LID-addressed groups to the top-level participant sender. (#19 — thanks @entropyy0)
|
||||
|
||||
@ -32,6 +32,7 @@ func newMessagesListCmd(flags *rootFlags) *cobra.Command {
|
||||
var fromMe bool
|
||||
var fromThem bool
|
||||
var asc bool
|
||||
var forwarded bool
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "list",
|
||||
@ -85,6 +86,7 @@ func newMessagesListCmd(flags *rootFlags) *cobra.Command {
|
||||
Before: before,
|
||||
FromMe: fromMeFilter,
|
||||
Asc: asc,
|
||||
Forwarded: forwarded,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
@ -109,6 +111,7 @@ func newMessagesListCmd(flags *rootFlags) *cobra.Command {
|
||||
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)")
|
||||
cmd.Flags().BoolVar(&forwarded, "forwarded", false, "only forwarded messages")
|
||||
return cmd
|
||||
}
|
||||
|
||||
@ -120,6 +123,7 @@ func newMessagesSearchCmd(flags *rootFlags) *cobra.Command {
|
||||
var beforeStr string
|
||||
var hasMedia bool
|
||||
var msgType string
|
||||
var forwarded bool
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "search <query>",
|
||||
@ -153,14 +157,15 @@ func newMessagesSearchCmd(flags *rootFlags) *cobra.Command {
|
||||
}
|
||||
|
||||
msgs, err := a.DB().SearchMessages(store.SearchMessagesParams{
|
||||
Query: args[0],
|
||||
ChatJID: chat,
|
||||
From: from,
|
||||
Limit: limit,
|
||||
After: after,
|
||||
Before: before,
|
||||
HasMedia: hasMedia,
|
||||
Type: msgType,
|
||||
Query: args[0],
|
||||
ChatJID: chat,
|
||||
From: from,
|
||||
Limit: limit,
|
||||
After: after,
|
||||
Before: before,
|
||||
HasMedia: hasMedia,
|
||||
Type: msgType,
|
||||
Forwarded: forwarded,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
@ -190,6 +195,7 @@ func newMessagesSearchCmd(flags *rootFlags) *cobra.Command {
|
||||
cmd.Flags().StringVar(&beforeStr, "before", "", "only messages before time (RFC3339 or YYYY-MM-DD)")
|
||||
cmd.Flags().BoolVar(&hasMedia, "has-media", false, "only messages with media")
|
||||
cmd.Flags().StringVar(&msgType, "type", "", "message type filter (text|image|video|audio|document)")
|
||||
cmd.Flags().BoolVar(&forwarded, "forwarded", false, "only forwarded messages")
|
||||
return cmd
|
||||
}
|
||||
|
||||
|
||||
@ -62,6 +62,12 @@ func writeMessageShow(dst io.Writer, m store.Message) error {
|
||||
if m.MediaType != "" {
|
||||
fmt.Fprintf(dst, "Media: %s\n", m.MediaType)
|
||||
}
|
||||
if m.IsForwarded {
|
||||
fmt.Fprintln(dst, "Forwarded: yes")
|
||||
if m.ForwardingScore > 0 {
|
||||
fmt.Fprintf(dst, "Forwarding score: %d\n", m.ForwardingScore)
|
||||
}
|
||||
}
|
||||
fmt.Fprintf(dst, "\n%s\n", m.Text)
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -98,7 +98,7 @@ func TestWriteMessagesListFullOutput(t *testing.T) {
|
||||
|
||||
func TestMessagesSearchCommandExposesMediaFilters(t *testing.T) {
|
||||
cmd := newMessagesSearchCmd(&rootFlags{})
|
||||
for _, name := range []string{"has-media", "type"} {
|
||||
for _, name := range []string{"has-media", "type", "forwarded"} {
|
||||
if cmd.Flags().Lookup(name) == nil {
|
||||
t.Fatalf("expected --%s flag", name)
|
||||
}
|
||||
@ -107,3 +107,33 @@ func TestMessagesSearchCommandExposesMediaFilters(t *testing.T) {
|
||||
t.Fatalf("type usage = %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMessagesListCommandExposesForwardedFilter(t *testing.T) {
|
||||
cmd := newMessagesListCmd(&rootFlags{})
|
||||
if cmd.Flags().Lookup("forwarded") == nil {
|
||||
t.Fatalf("expected --forwarded flag")
|
||||
}
|
||||
}
|
||||
|
||||
func TestWriteMessageShowIncludesForwardedMetadata(t *testing.T) {
|
||||
msg := store.Message{
|
||||
ChatJID: "chat@s.whatsapp.net",
|
||||
SenderJID: "sender@s.whatsapp.net",
|
||||
MsgID: "mid",
|
||||
Timestamp: time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC),
|
||||
Text: "hello",
|
||||
IsForwarded: true,
|
||||
ForwardingScore: 3,
|
||||
}
|
||||
|
||||
var out bytes.Buffer
|
||||
if err := writeMessageShow(&out, msg); err != nil {
|
||||
t.Fatalf("writeMessageShow: %v", err)
|
||||
}
|
||||
if !strings.Contains(out.String(), "Forwarded: yes") {
|
||||
t.Fatalf("expected forwarded marker, got:\n%s", out.String())
|
||||
}
|
||||
if !strings.Contains(out.String(), "Forwarding score: 3") {
|
||||
t.Fatalf("expected forwarding score, got:\n%s", out.String())
|
||||
}
|
||||
}
|
||||
|
||||
@ -203,24 +203,26 @@ func (a *App) storeParsedMessage(ctx context.Context, pm wa.ParsedMessage) error
|
||||
displayText := a.buildDisplayText(ctx, pm)
|
||||
|
||||
return a.db.UpsertMessage(store.UpsertMessageParams{
|
||||
ChatJID: chatJID,
|
||||
ChatName: chatName,
|
||||
MsgID: pm.ID,
|
||||
SenderJID: senderJID,
|
||||
SenderName: senderName,
|
||||
Timestamp: pm.Timestamp,
|
||||
FromMe: pm.FromMe,
|
||||
Text: pm.Text,
|
||||
DisplayText: displayText,
|
||||
MediaType: mediaType,
|
||||
MediaCaption: caption,
|
||||
Filename: filename,
|
||||
MimeType: mimeType,
|
||||
DirectPath: directPath,
|
||||
MediaKey: mediaKey,
|
||||
FileSHA256: fileSha,
|
||||
FileEncSHA256: fileEncSha,
|
||||
FileLength: fileLen,
|
||||
ChatJID: chatJID,
|
||||
ChatName: chatName,
|
||||
MsgID: pm.ID,
|
||||
SenderJID: senderJID,
|
||||
SenderName: senderName,
|
||||
Timestamp: pm.Timestamp,
|
||||
FromMe: pm.FromMe,
|
||||
Text: pm.Text,
|
||||
DisplayText: displayText,
|
||||
IsForwarded: pm.IsForwarded,
|
||||
ForwardingScore: pm.ForwardingScore,
|
||||
MediaType: mediaType,
|
||||
MediaCaption: caption,
|
||||
Filename: filename,
|
||||
MimeType: mimeType,
|
||||
DirectPath: directPath,
|
||||
MediaKey: mediaKey,
|
||||
FileSHA256: fileSha,
|
||||
FileEncSHA256: fileEncSha,
|
||||
FileLength: fileLen,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@ -211,6 +211,37 @@ func TestStoreParsedMessageResolvesLIDChatAndSender(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestStoreParsedMessageStoresForwardedMetadata(t *testing.T) {
|
||||
a := newTestApp(t)
|
||||
f := newFakeWA()
|
||||
a.wa = f
|
||||
|
||||
chat := types.JID{User: "123", Server: types.DefaultUserServer}
|
||||
err := a.storeParsedMessage(context.Background(), wa.ParsedMessage{
|
||||
Chat: chat,
|
||||
ID: "m-forwarded",
|
||||
SenderJID: chat.String(),
|
||||
Timestamp: time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC),
|
||||
Text: "forwarded",
|
||||
IsForwarded: true,
|
||||
ForwardingScore: 4,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("storeParsedMessage: %v", err)
|
||||
}
|
||||
|
||||
msg, err := a.db.GetMessage(chat.String(), "m-forwarded")
|
||||
if err != nil {
|
||||
t.Fatalf("GetMessage: %v", err)
|
||||
}
|
||||
if !msg.IsForwarded {
|
||||
t.Fatalf("expected forwarded message, got %+v", msg)
|
||||
}
|
||||
if msg.ForwardingScore != 4 {
|
||||
t.Fatalf("ForwardingScore = %d, want 4", msg.ForwardingScore)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSyncStoresDisplayText(t *testing.T) {
|
||||
a := newTestApp(t)
|
||||
f := newFakeWA()
|
||||
|
||||
@ -7,33 +7,36 @@ import (
|
||||
)
|
||||
|
||||
type UpsertMessageParams struct {
|
||||
ChatJID string
|
||||
ChatName string
|
||||
MsgID string
|
||||
SenderJID string
|
||||
SenderName string
|
||||
Timestamp time.Time
|
||||
FromMe bool
|
||||
Text string
|
||||
DisplayText string
|
||||
MediaType string
|
||||
MediaCaption string
|
||||
Filename string
|
||||
MimeType string
|
||||
DirectPath string
|
||||
MediaKey []byte
|
||||
FileSHA256 []byte
|
||||
FileEncSHA256 []byte
|
||||
FileLength uint64
|
||||
ChatJID string
|
||||
ChatName string
|
||||
MsgID string
|
||||
SenderJID string
|
||||
SenderName string
|
||||
Timestamp time.Time
|
||||
FromMe bool
|
||||
Text string
|
||||
DisplayText string
|
||||
IsForwarded bool
|
||||
ForwardingScore uint32
|
||||
MediaType string
|
||||
MediaCaption string
|
||||
Filename string
|
||||
MimeType string
|
||||
DirectPath string
|
||||
MediaKey []byte
|
||||
FileSHA256 []byte
|
||||
FileEncSHA256 []byte
|
||||
FileLength uint64
|
||||
}
|
||||
|
||||
func (d *DB) UpsertMessage(p UpsertMessageParams) error {
|
||||
_, err := d.sql.Exec(`
|
||||
INSERT INTO messages(
|
||||
chat_jid, chat_name, msg_id, sender_jid, sender_name, ts, from_me, text, display_text,
|
||||
is_forwarded, forwarding_score,
|
||||
media_type, media_caption, filename, mime_type, direct_path,
|
||||
media_key, file_sha256, file_enc_sha256, file_length
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
ON CONFLICT(chat_jid, msg_id) DO UPDATE SET
|
||||
chat_name=COALESCE(NULLIF(excluded.chat_name,''), messages.chat_name),
|
||||
sender_jid=excluded.sender_jid,
|
||||
@ -42,6 +45,8 @@ func (d *DB) UpsertMessage(p UpsertMessageParams) error {
|
||||
from_me=excluded.from_me,
|
||||
text=excluded.text,
|
||||
display_text=CASE WHEN excluded.display_text IS NOT NULL AND excluded.display_text != '' THEN excluded.display_text ELSE messages.display_text END,
|
||||
is_forwarded=excluded.is_forwarded,
|
||||
forwarding_score=excluded.forwarding_score,
|
||||
media_type=excluded.media_type,
|
||||
media_caption=excluded.media_caption,
|
||||
filename=COALESCE(NULLIF(excluded.filename,''), messages.filename),
|
||||
@ -52,6 +57,7 @@ func (d *DB) UpsertMessage(p UpsertMessageParams) error {
|
||||
file_enc_sha256=CASE WHEN excluded.file_enc_sha256 IS NOT NULL AND length(excluded.file_enc_sha256)>0 THEN excluded.file_enc_sha256 ELSE messages.file_enc_sha256 END,
|
||||
file_length=CASE WHEN excluded.file_length>0 THEN excluded.file_length ELSE messages.file_length END
|
||||
`, p.ChatJID, nullIfEmpty(p.ChatName), p.MsgID, nullIfEmpty(p.SenderJID), nullIfEmpty(p.SenderName), unix(p.Timestamp), boolToInt(p.FromMe), nullIfEmpty(p.Text), nullIfEmpty(p.DisplayText),
|
||||
boolToInt(p.IsForwarded), int64(p.ForwardingScore),
|
||||
nullIfEmpty(p.MediaType), nullIfEmpty(p.MediaCaption), nullIfEmpty(p.Filename), nullIfEmpty(p.MimeType), nullIfEmpty(p.DirectPath),
|
||||
p.MediaKey, p.FileSHA256, p.FileEncSHA256, int64(p.FileLength),
|
||||
)
|
||||
@ -66,6 +72,7 @@ type ListMessagesParams struct {
|
||||
After *time.Time
|
||||
FromMe *bool
|
||||
Asc bool
|
||||
Forwarded bool
|
||||
}
|
||||
|
||||
func (d *DB) ListMessages(p ListMessagesParams) ([]Message, error) {
|
||||
@ -73,7 +80,7 @@ func (d *DB) ListMessages(p ListMessagesParams) ([]Message, error) {
|
||||
p.Limit = 50
|
||||
}
|
||||
query := `
|
||||
SELECT m.rowid, m.chat_jid, COALESCE(c.name,''), m.msg_id, COALESCE(m.sender_jid,''), m.ts, m.from_me, COALESCE(m.text,''), COALESCE(m.display_text,''), COALESCE(m.media_type,''), ''
|
||||
SELECT m.rowid, m.chat_jid, COALESCE(c.name,''), m.msg_id, COALESCE(m.sender_jid,''), m.ts, m.from_me, COALESCE(m.text,''), COALESCE(m.display_text,''), m.is_forwarded, m.forwarding_score, COALESCE(m.media_type,''), ''
|
||||
FROM messages m
|
||||
LEFT JOIN chats c ON c.jid = m.chat_jid
|
||||
WHERE 1=1`
|
||||
@ -98,6 +105,9 @@ func (d *DB) ListMessages(p ListMessagesParams) ([]Message, error) {
|
||||
query += " AND m.from_me = ?"
|
||||
args = append(args, boolToInt(*p.FromMe))
|
||||
}
|
||||
if p.Forwarded {
|
||||
query += " AND m.is_forwarded = 1"
|
||||
}
|
||||
if p.Asc {
|
||||
query += " ORDER BY m.ts ASC, m.rowid ASC LIMIT ?"
|
||||
} else {
|
||||
@ -109,7 +119,7 @@ func (d *DB) ListMessages(p ListMessagesParams) ([]Message, error) {
|
||||
|
||||
func (d *DB) GetMessage(chatJID, msgID string) (Message, error) {
|
||||
row := d.sql.QueryRow(`
|
||||
SELECT m.rowid, m.chat_jid, COALESCE(c.name,''), m.msg_id, COALESCE(m.sender_jid,''), m.ts, m.from_me, COALESCE(m.text,''), COALESCE(m.display_text,''), COALESCE(m.media_type,''), ''
|
||||
SELECT m.rowid, m.chat_jid, COALESCE(c.name,''), m.msg_id, COALESCE(m.sender_jid,''), m.ts, m.from_me, COALESCE(m.text,''), COALESCE(m.display_text,''), m.is_forwarded, m.forwarding_score, COALESCE(m.media_type,''), ''
|
||||
FROM messages m
|
||||
LEFT JOIN chats c ON c.jid = m.chat_jid
|
||||
WHERE m.chat_jid = ? AND m.msg_id = ?
|
||||
@ -117,11 +127,15 @@ func (d *DB) GetMessage(chatJID, msgID string) (Message, error) {
|
||||
var m Message
|
||||
var ts int64
|
||||
var fromMe int
|
||||
if err := row.Scan(&m.rowID, &m.ChatJID, &m.ChatName, &m.MsgID, &m.SenderJID, &ts, &fromMe, &m.Text, &m.DisplayText, &m.MediaType, &m.Snippet); err != nil {
|
||||
var forwarded int
|
||||
var forwardingScore int64
|
||||
if err := row.Scan(&m.rowID, &m.ChatJID, &m.ChatName, &m.MsgID, &m.SenderJID, &ts, &fromMe, &m.Text, &m.DisplayText, &forwarded, &forwardingScore, &m.MediaType, &m.Snippet); err != nil {
|
||||
return Message{}, err
|
||||
}
|
||||
m.Timestamp = fromUnix(ts)
|
||||
m.FromMe = fromMe != 0
|
||||
m.IsForwarded = forwarded != 0
|
||||
m.ForwardingScore = uint32(forwardingScore)
|
||||
return m, nil
|
||||
}
|
||||
|
||||
@ -170,7 +184,7 @@ func (d *DB) MessageContext(chatJID, msgID string, before, after int) ([]Message
|
||||
}
|
||||
|
||||
beforeRows, err := d.scanMessages(`
|
||||
SELECT m.rowid, m.chat_jid, COALESCE(c.name,''), m.msg_id, COALESCE(m.sender_jid,''), m.ts, m.from_me, COALESCE(m.text,''), COALESCE(m.display_text,''), COALESCE(m.media_type,''), ''
|
||||
SELECT m.rowid, m.chat_jid, COALESCE(c.name,''), m.msg_id, COALESCE(m.sender_jid,''), m.ts, m.from_me, COALESCE(m.text,''), COALESCE(m.display_text,''), m.is_forwarded, m.forwarding_score, COALESCE(m.media_type,''), ''
|
||||
FROM messages m
|
||||
LEFT JOIN chats c ON c.jid = m.chat_jid
|
||||
WHERE m.chat_jid = ? AND (m.ts < ? OR (m.ts = ? AND m.rowid < ?))
|
||||
@ -182,7 +196,7 @@ func (d *DB) MessageContext(chatJID, msgID string, before, after int) ([]Message
|
||||
}
|
||||
|
||||
afterRows, err := d.scanMessages(`
|
||||
SELECT m.rowid, m.chat_jid, COALESCE(c.name,''), m.msg_id, COALESCE(m.sender_jid,''), m.ts, m.from_me, COALESCE(m.text,''), COALESCE(m.display_text,''), COALESCE(m.media_type,''), ''
|
||||
SELECT m.rowid, m.chat_jid, COALESCE(c.name,''), m.msg_id, COALESCE(m.sender_jid,''), m.ts, m.from_me, COALESCE(m.text,''), COALESCE(m.display_text,''), m.is_forwarded, m.forwarding_score, COALESCE(m.media_type,''), ''
|
||||
FROM messages m
|
||||
LEFT JOIN chats c ON c.jid = m.chat_jid
|
||||
WHERE m.chat_jid = ? AND (m.ts > ? OR (m.ts = ? AND m.rowid > ?))
|
||||
@ -217,11 +231,15 @@ func (d *DB) scanMessages(query string, args ...interface{}) ([]Message, error)
|
||||
var m Message
|
||||
var ts int64
|
||||
var fromMe int
|
||||
if err := rows.Scan(&m.rowID, &m.ChatJID, &m.ChatName, &m.MsgID, &m.SenderJID, &ts, &fromMe, &m.Text, &m.DisplayText, &m.MediaType, &m.Snippet); err != nil {
|
||||
var forwarded int
|
||||
var forwardingScore int64
|
||||
if err := rows.Scan(&m.rowID, &m.ChatJID, &m.ChatName, &m.MsgID, &m.SenderJID, &ts, &fromMe, &m.Text, &m.DisplayText, &forwarded, &forwardingScore, &m.MediaType, &m.Snippet); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
m.Timestamp = fromUnix(ts)
|
||||
m.FromMe = fromMe != 0
|
||||
m.IsForwarded = forwarded != 0
|
||||
m.ForwardingScore = uint32(forwardingScore)
|
||||
out = append(out, m)
|
||||
}
|
||||
return out, rows.Err()
|
||||
|
||||
@ -81,7 +81,8 @@ func TestListMessagesFiltersAndOrdering(t *testing.T) {
|
||||
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"},
|
||||
{ChatJID: chat, MsgID: "forwarded", SenderJID: "bob@s.whatsapp.net", Timestamp: base.Add(2 * time.Second), Text: "forwarded", IsForwarded: true, ForwardingScore: 2},
|
||||
{ChatJID: otherChat, MsgID: "other-chat", SenderJID: "alice@s.whatsapp.net", Timestamp: base.Add(3 * time.Second), Text: "other"},
|
||||
}
|
||||
for _, row := range rows {
|
||||
if err := db.UpsertMessage(row); err != nil {
|
||||
@ -93,7 +94,7 @@ func TestListMessagesFiltersAndOrdering(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("ListMessages: %v", err)
|
||||
}
|
||||
if got := messageIDs(msgs); got != "new-from-me,old-from-alice" {
|
||||
if got := messageIDs(msgs); got != "forwarded,new-from-me,old-from-alice" {
|
||||
t.Fatalf("default order = %s", got)
|
||||
}
|
||||
|
||||
@ -101,7 +102,7 @@ func TestListMessagesFiltersAndOrdering(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("ListMessages asc: %v", err)
|
||||
}
|
||||
if got := messageIDs(msgs); got != "old-from-alice,new-from-me" {
|
||||
if got := messageIDs(msgs); got != "old-from-alice,new-from-me,forwarded" {
|
||||
t.Fatalf("asc order = %s", got)
|
||||
}
|
||||
|
||||
@ -121,6 +122,17 @@ func TestListMessagesFiltersAndOrdering(t *testing.T) {
|
||||
if got := messageIDs(msgs); got != "old-from-alice" {
|
||||
t.Fatalf("sender filter = %s", got)
|
||||
}
|
||||
|
||||
msgs, err = db.ListMessages(ListMessagesParams{ChatJID: chat, Limit: 10, Forwarded: true})
|
||||
if err != nil {
|
||||
t.Fatalf("ListMessages forwarded: %v", err)
|
||||
}
|
||||
if got := messageIDs(msgs); got != "forwarded" {
|
||||
t.Fatalf("forwarded filter = %s", got)
|
||||
}
|
||||
if msgs[0].ForwardingScore != 2 {
|
||||
t.Fatalf("ForwardingScore = %d, want 2", msgs[0].ForwardingScore)
|
||||
}
|
||||
}
|
||||
|
||||
func TestListMessagesStableSameTimestampOrder(t *testing.T) {
|
||||
|
||||
@ -18,6 +18,7 @@ var schemaMigrations = []migration{
|
||||
{version: 2, name: "messages display_text column", up: migrateMessagesDisplayText},
|
||||
{version: 3, name: "messages fts", up: migrateMessagesFTS},
|
||||
{version: 4, name: "groups left_at column", up: migrateGroupsLeftAt},
|
||||
{version: 5, name: "messages forwarded columns", up: migrateMessagesForwardedColumns},
|
||||
}
|
||||
|
||||
func (d *DB) ensureSchema() error {
|
||||
@ -97,6 +98,29 @@ func migrateMessagesDisplayText(d *DB) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func migrateMessagesForwardedColumns(d *DB) error {
|
||||
hasForwarded, err := d.tableHasColumn("messages", "is_forwarded")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !hasForwarded {
|
||||
if _, err := d.sql.Exec(`ALTER TABLE messages ADD COLUMN is_forwarded INTEGER NOT NULL DEFAULT 0`); err != nil {
|
||||
return fmt.Errorf("add messages.is_forwarded column: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
hasScore, err := d.tableHasColumn("messages", "forwarding_score")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !hasScore {
|
||||
if _, err := d.sql.Exec(`ALTER TABLE messages ADD COLUMN forwarding_score INTEGER NOT NULL DEFAULT 0`); err != nil {
|
||||
return fmt.Errorf("add messages.forwarding_score column: %w", err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func migrateMessagesFTS(d *DB) error {
|
||||
ftsExists, err := d.tableExists("messages_fts")
|
||||
if err != nil {
|
||||
|
||||
@ -63,6 +63,8 @@ const coreSchemaSQL = `
|
||||
from_me INTEGER NOT NULL,
|
||||
text TEXT,
|
||||
display_text TEXT,
|
||||
is_forwarded INTEGER NOT NULL DEFAULT 0,
|
||||
forwarding_score INTEGER NOT NULL DEFAULT 0,
|
||||
media_type TEXT,
|
||||
media_caption TEXT,
|
||||
filename TEXT,
|
||||
|
||||
@ -26,6 +26,8 @@ func TestOpenCreatesExpectedSchema(t *testing.T) {
|
||||
"chat_name",
|
||||
"sender_name",
|
||||
"display_text",
|
||||
"is_forwarded",
|
||||
"forwarding_score",
|
||||
"local_path",
|
||||
"downloaded_at",
|
||||
} {
|
||||
|
||||
@ -7,14 +7,15 @@ import (
|
||||
)
|
||||
|
||||
type SearchMessagesParams struct {
|
||||
Query string
|
||||
ChatJID string
|
||||
From string
|
||||
Limit int
|
||||
Before *time.Time
|
||||
After *time.Time
|
||||
HasMedia bool
|
||||
Type string
|
||||
Query string
|
||||
ChatJID string
|
||||
From string
|
||||
Limit int
|
||||
Before *time.Time
|
||||
After *time.Time
|
||||
HasMedia bool
|
||||
Type string
|
||||
Forwarded bool
|
||||
}
|
||||
|
||||
func (d *DB) SearchMessages(p SearchMessagesParams) ([]Message, error) {
|
||||
@ -53,7 +54,7 @@ func likeContains(s string) string {
|
||||
|
||||
func (d *DB) searchLIKE(p SearchMessagesParams) ([]Message, error) {
|
||||
query := `
|
||||
SELECT m.rowid, m.chat_jid, COALESCE(c.name,''), m.msg_id, COALESCE(m.sender_jid,''), m.ts, m.from_me, COALESCE(m.text,''), COALESCE(m.display_text,''), COALESCE(m.media_type,''), ''
|
||||
SELECT m.rowid, m.chat_jid, COALESCE(c.name,''), m.msg_id, COALESCE(m.sender_jid,''), m.ts, m.from_me, COALESCE(m.text,''), COALESCE(m.display_text,''), m.is_forwarded, m.forwarding_score, COALESCE(m.media_type,''), ''
|
||||
FROM messages m
|
||||
LEFT JOIN chats c ON c.jid = m.chat_jid
|
||||
WHERE (LOWER(m.text) LIKE LOWER(?) ESCAPE '\' OR LOWER(m.display_text) LIKE LOWER(?) ESCAPE '\' OR LOWER(m.media_caption) LIKE LOWER(?) ESCAPE '\' OR LOWER(m.filename) LIKE LOWER(?) ESCAPE '\' OR LOWER(COALESCE(m.chat_name,'')) LIKE LOWER(?) ESCAPE '\' OR LOWER(COALESCE(m.sender_name,'')) LIKE LOWER(?) ESCAPE '\' OR LOWER(COALESCE(c.name,'')) LIKE LOWER(?) ESCAPE '\')`
|
||||
@ -86,7 +87,7 @@ func sanitizeFTSQuery(q string) string {
|
||||
|
||||
func (d *DB) searchFTS(p SearchMessagesParams) ([]Message, error) {
|
||||
query := `
|
||||
SELECT m.rowid, m.chat_jid, COALESCE(c.name,''), m.msg_id, COALESCE(m.sender_jid,''), m.ts, m.from_me, COALESCE(m.text,''), COALESCE(m.display_text,''), COALESCE(m.media_type,''),
|
||||
SELECT m.rowid, m.chat_jid, COALESCE(c.name,''), m.msg_id, COALESCE(m.sender_jid,''), m.ts, m.from_me, COALESCE(m.text,''), COALESCE(m.display_text,''), m.is_forwarded, m.forwarding_score, COALESCE(m.media_type,''),
|
||||
snippet(messages_fts, 0, '[', ']', '…', 12)
|
||||
FROM messages_fts
|
||||
JOIN messages m ON messages_fts.rowid = m.rowid
|
||||
@ -122,6 +123,9 @@ func applyMessageFilters(query string, args []interface{}, p SearchMessagesParam
|
||||
if p.HasMedia {
|
||||
query += " AND COALESCE(m.media_type,'') != ''"
|
||||
}
|
||||
if p.Forwarded {
|
||||
query += " AND m.is_forwarded = 1"
|
||||
}
|
||||
if msgType := normalizedMessageType(p.Type); msgType != "" {
|
||||
if msgType == "text" {
|
||||
query += " AND COALESCE(m.media_type,'') = ''"
|
||||
|
||||
@ -49,6 +49,17 @@ func TestSearchMessagesFiltersByMediaAndType(t *testing.T) {
|
||||
Filename: "report.pdf",
|
||||
MimeType: "application/pdf",
|
||||
},
|
||||
{
|
||||
ChatJID: chat,
|
||||
ChatName: "Alice",
|
||||
MsgID: "forwarded-1",
|
||||
SenderJID: chat,
|
||||
SenderName: "Alice",
|
||||
Timestamp: base.Add(3 * time.Second),
|
||||
Text: "forwarded memo",
|
||||
IsForwarded: true,
|
||||
ForwardingScore: 1,
|
||||
},
|
||||
}
|
||||
for _, row := range rows {
|
||||
if err := db.UpsertMessage(row); err != nil {
|
||||
@ -86,6 +97,11 @@ func TestSearchMessagesFiltersByMediaAndType(t *testing.T) {
|
||||
p: SearchMessagesParams{Query: "report", Limit: 10, HasMedia: true, Type: "document"},
|
||||
want: "document-1",
|
||||
},
|
||||
{
|
||||
name: "forwarded",
|
||||
p: SearchMessagesParams{Query: "forwarded", Limit: 10, Forwarded: true},
|
||||
want: "forwarded-1",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
|
||||
@ -47,17 +47,19 @@ type MediaDownloadInfo struct {
|
||||
}
|
||||
|
||||
type Message struct {
|
||||
ChatJID string
|
||||
ChatName string
|
||||
MsgID string
|
||||
SenderJID string
|
||||
Timestamp time.Time
|
||||
FromMe bool
|
||||
Text string
|
||||
DisplayText string
|
||||
MediaType string
|
||||
Snippet string
|
||||
rowID int64
|
||||
ChatJID string
|
||||
ChatName string
|
||||
MsgID string
|
||||
SenderJID string
|
||||
Timestamp time.Time
|
||||
FromMe bool
|
||||
Text string
|
||||
DisplayText string
|
||||
IsForwarded bool
|
||||
ForwardingScore uint32
|
||||
MediaType string
|
||||
Snippet string
|
||||
rowID int64
|
||||
}
|
||||
|
||||
type MessageInfo struct {
|
||||
|
||||
@ -22,18 +22,20 @@ type Media struct {
|
||||
}
|
||||
|
||||
type ParsedMessage struct {
|
||||
Chat types.JID
|
||||
ID string
|
||||
SenderJID string
|
||||
Timestamp time.Time
|
||||
FromMe bool
|
||||
Text string
|
||||
Media *Media
|
||||
PushName string
|
||||
ReplyToID string
|
||||
ReplyToDisplay string
|
||||
ReactionToID string
|
||||
ReactionEmoji string
|
||||
Chat types.JID
|
||||
ID string
|
||||
SenderJID string
|
||||
Timestamp time.Time
|
||||
FromMe bool
|
||||
Text string
|
||||
Media *Media
|
||||
PushName string
|
||||
ReplyToID string
|
||||
ReplyToDisplay string
|
||||
ReactionToID string
|
||||
ReactionEmoji string
|
||||
IsForwarded bool
|
||||
ForwardingScore uint32
|
||||
}
|
||||
|
||||
func ParseLiveMessage(evt *events.Message) ParsedMessage {
|
||||
@ -97,6 +99,8 @@ func extractWAProto(m *waProto.Message, pm *ParsedMessage) {
|
||||
if quoted := ctx.GetQuotedMessage(); quoted != nil {
|
||||
pm.ReplyToDisplay = strings.TrimSpace(displayTextForProto(quoted))
|
||||
}
|
||||
pm.ForwardingScore = ctx.GetForwardingScore()
|
||||
pm.IsForwarded = ctx.GetIsForwarded() || pm.ForwardingScore > 0
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -180,6 +180,41 @@ func TestParseLiveMessageReply(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseLiveMessageForwarded(t *testing.T) {
|
||||
chat, _ := types.ParseJID("123@s.whatsapp.net")
|
||||
sender, _ := types.ParseJID("sender@s.whatsapp.net")
|
||||
|
||||
ev := &events.Message{
|
||||
Info: types.MessageInfo{
|
||||
MessageSource: types.MessageSource{
|
||||
Chat: chat,
|
||||
Sender: sender,
|
||||
IsFromMe: false,
|
||||
},
|
||||
ID: "mid",
|
||||
Timestamp: time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC),
|
||||
PushName: "Sender",
|
||||
},
|
||||
Message: &waProto.Message{
|
||||
ExtendedTextMessage: &waProto.ExtendedTextMessage{
|
||||
Text: proto.String("forwarded text"),
|
||||
ContextInfo: &waProto.ContextInfo{
|
||||
IsForwarded: proto.Bool(true),
|
||||
ForwardingScore: proto.Uint32(3),
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
pm := ParseLiveMessage(ev)
|
||||
if !pm.IsForwarded {
|
||||
t.Fatalf("expected forwarded message, got %+v", pm)
|
||||
}
|
||||
if pm.ForwardingScore != 3 {
|
||||
t.Fatalf("ForwardingScore = %d, want 3", pm.ForwardingScore)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseTemplateMessage(t *testing.T) {
|
||||
chat, _ := types.ParseJID("123@s.whatsapp.net")
|
||||
sender, _ := types.ParseJID("biz@s.whatsapp.net")
|
||||
|
||||
Loading…
Reference in New Issue
Block a user