Compare commits
2 Commits
main
...
display-te
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3a2561aa11 | ||
|
|
d360a966f9 |
@ -6,6 +6,7 @@
|
||||
|
||||
- Start tracking changes for the next release (from 2026-01-23).
|
||||
- Send: `wacli send file --filename` to override display name for uploads. (#7 — thanks @plattenschieber)
|
||||
- Messages: preserve captions in list/search output while still indexing display text for reactions/media. (#5 — thanks @zats)
|
||||
|
||||
### Fixed
|
||||
|
||||
|
||||
@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"text/tabwriter"
|
||||
"time"
|
||||
|
||||
@ -88,9 +89,12 @@ func newMessagesListCmd(flags *rootFlags) *cobra.Command {
|
||||
if chatLabel == "" {
|
||||
chatLabel = m.ChatJID
|
||||
}
|
||||
text := m.Text
|
||||
text := strings.TrimSpace(m.Text)
|
||||
if text == "" {
|
||||
text = strings.TrimSpace(m.DisplayText)
|
||||
}
|
||||
if m.MediaType != "" && text == "" {
|
||||
text = "[" + m.MediaType + "]"
|
||||
text = "Sent " + m.MediaType
|
||||
}
|
||||
fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\n",
|
||||
m.Timestamp.Local().Format("2006-01-02 15:04:05"),
|
||||
@ -183,6 +187,12 @@ func newMessagesSearchCmd(flags *rootFlags) *cobra.Command {
|
||||
chatLabel = m.ChatJID
|
||||
}
|
||||
match := m.Snippet
|
||||
if match == "" {
|
||||
match = strings.TrimSpace(m.Text)
|
||||
}
|
||||
if match == "" {
|
||||
match = strings.TrimSpace(m.DisplayText)
|
||||
}
|
||||
if match == "" {
|
||||
match = m.Text
|
||||
}
|
||||
|
||||
@ -12,6 +12,7 @@ import (
|
||||
"go.mau.fi/whatsmeow"
|
||||
waProto "go.mau.fi/whatsmeow/binary/proto"
|
||||
"go.mau.fi/whatsmeow/types"
|
||||
"go.mau.fi/whatsmeow/types/events"
|
||||
)
|
||||
|
||||
type WAClient interface {
|
||||
@ -41,6 +42,7 @@ type WAClient interface {
|
||||
Upload(ctx context.Context, data []byte, mediaType whatsmeow.MediaType) (whatsmeow.UploadResponse, error)
|
||||
DownloadMediaToFile(ctx context.Context, directPath string, encFileHash, fileHash, mediaKey []byte, fileLength uint64, mediaType, mmsType string, targetPath string) (int64, error)
|
||||
|
||||
DecryptReaction(ctx context.Context, reaction *events.Message) (*waProto.ReactionMessage, error)
|
||||
RequestHistorySyncOnDemand(ctx context.Context, lastKnown types.MessageInfo, count int) (types.MessageID, error)
|
||||
Logout(ctx context.Context) error
|
||||
}
|
||||
|
||||
@ -216,6 +216,10 @@ func (f *fakeWA) Upload(ctx context.Context, data []byte, mediaType whatsmeow.Me
|
||||
return whatsmeow.UploadResponse{}, nil
|
||||
}
|
||||
|
||||
func (f *fakeWA) DecryptReaction(ctx context.Context, reaction *events.Message) (*waProto.ReactionMessage, error) {
|
||||
return nil, fmt.Errorf("not supported")
|
||||
}
|
||||
|
||||
func (f *fakeWA) DownloadMediaToFile(ctx context.Context, directPath string, encFileHash, fileHash, mediaKey []byte, fileLength uint64, mediaType, mmsType string, targetPath string) (int64, error) {
|
||||
if err := os.MkdirAll(filepath.Dir(targetPath), 0o700); err != nil {
|
||||
return 0, err
|
||||
|
||||
@ -85,6 +85,16 @@ func (a *App) Sync(ctx context.Context, opts SyncOptions) (SyncResult, error) {
|
||||
switch v := evt.(type) {
|
||||
case *events.Message:
|
||||
pm := wa.ParseLiveMessage(v)
|
||||
if pm.ReactionToID != "" && pm.ReactionEmoji == "" && v.Message != nil && v.Message.GetEncReactionMessage() != nil {
|
||||
if reaction, err := a.wa.DecryptReaction(ctx, v); err == nil && reaction != nil {
|
||||
pm.ReactionEmoji = reaction.GetText()
|
||||
if pm.ReactionToID == "" {
|
||||
if key := reaction.GetKey(); key != nil {
|
||||
pm.ReactionToID = key.GetID()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if err := a.storeParsedMessage(ctx, pm); err == nil {
|
||||
messagesStored.Add(1)
|
||||
}
|
||||
@ -295,6 +305,8 @@ func (a *App) storeParsedMessage(ctx context.Context, pm wa.ParsedMessage) error
|
||||
fileLen = pm.Media.FileLength
|
||||
}
|
||||
|
||||
displayText := a.buildDisplayText(ctx, pm)
|
||||
|
||||
return a.db.UpsertMessage(store.UpsertMessageParams{
|
||||
ChatJID: chatJID,
|
||||
ChatName: chatName,
|
||||
@ -304,6 +316,7 @@ func (a *App) storeParsedMessage(ctx context.Context, pm wa.ParsedMessage) error
|
||||
Timestamp: pm.Timestamp,
|
||||
FromMe: pm.FromMe,
|
||||
Text: pm.Text,
|
||||
DisplayText: displayText,
|
||||
MediaType: mediaType,
|
||||
MediaCaption: caption,
|
||||
Filename: filename,
|
||||
@ -315,3 +328,100 @@ func (a *App) storeParsedMessage(ctx context.Context, pm wa.ParsedMessage) error
|
||||
FileLength: fileLen,
|
||||
})
|
||||
}
|
||||
|
||||
func (a *App) buildDisplayText(ctx context.Context, pm wa.ParsedMessage) string {
|
||||
base := baseDisplayText(pm)
|
||||
|
||||
if pm.ReactionToID != "" || strings.TrimSpace(pm.ReactionEmoji) != "" {
|
||||
target := strings.TrimSpace(pm.ReactionToID)
|
||||
display := ""
|
||||
if target != "" {
|
||||
display = a.lookupMessageDisplayText(pm.Chat.String(), target)
|
||||
}
|
||||
if display == "" {
|
||||
display = "message"
|
||||
}
|
||||
emoji := strings.TrimSpace(pm.ReactionEmoji)
|
||||
if emoji != "" {
|
||||
return fmt.Sprintf("Reacted %s to %s", emoji, display)
|
||||
}
|
||||
return fmt.Sprintf("Reacted to %s", display)
|
||||
}
|
||||
|
||||
if pm.ReplyToID != "" {
|
||||
quoted := strings.TrimSpace(pm.ReplyToDisplay)
|
||||
if quoted == "" {
|
||||
quoted = a.lookupMessageDisplayText(pm.Chat.String(), pm.ReplyToID)
|
||||
}
|
||||
if quoted == "" {
|
||||
quoted = "message"
|
||||
}
|
||||
if base == "" {
|
||||
base = "(message)"
|
||||
}
|
||||
return fmt.Sprintf("> %s\n%s", quoted, base)
|
||||
}
|
||||
|
||||
if base == "" {
|
||||
base = "(message)"
|
||||
}
|
||||
return base
|
||||
}
|
||||
|
||||
func baseDisplayText(pm wa.ParsedMessage) string {
|
||||
if pm.Media != nil {
|
||||
return "Sent " + mediaLabel(pm.Media.Type)
|
||||
}
|
||||
if text := strings.TrimSpace(pm.Text); text != "" {
|
||||
return text
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (a *App) lookupMessageDisplayText(chatJID, msgID string) string {
|
||||
if strings.TrimSpace(chatJID) == "" || strings.TrimSpace(msgID) == "" {
|
||||
return ""
|
||||
}
|
||||
msg, err := a.db.GetMessage(chatJID, msgID)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
if text := strings.TrimSpace(msg.DisplayText); text != "" {
|
||||
return text
|
||||
}
|
||||
if text := strings.TrimSpace(msg.Text); text != "" {
|
||||
return text
|
||||
}
|
||||
if strings.TrimSpace(msg.MediaType) != "" {
|
||||
return "Sent " + mediaLabel(msg.MediaType)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func mediaLabel(mediaType string) string {
|
||||
mt := strings.ToLower(strings.TrimSpace(mediaType))
|
||||
switch mt {
|
||||
case "gif":
|
||||
return "gif"
|
||||
case "image":
|
||||
return "image"
|
||||
case "video":
|
||||
return "video"
|
||||
case "audio":
|
||||
return "audio"
|
||||
case "sticker":
|
||||
return "sticker"
|
||||
case "document":
|
||||
return "document"
|
||||
case "location":
|
||||
return "location"
|
||||
case "contact":
|
||||
return "contact"
|
||||
case "contacts":
|
||||
return "contacts"
|
||||
case "":
|
||||
return "message"
|
||||
default:
|
||||
return mt
|
||||
}
|
||||
}
|
||||
|
||||
@ -85,6 +85,156 @@ func TestSyncStoresLiveAndHistoryMessages(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestSyncStoresDisplayText(t *testing.T) {
|
||||
a := newTestApp(t)
|
||||
f := newFakeWA()
|
||||
a.wa = f
|
||||
|
||||
chat := types.JID{User: "123", Server: types.DefaultUserServer}
|
||||
f.contacts[chat.ToNonAD()] = types.ContactInfo{
|
||||
Found: true,
|
||||
FullName: "Alice",
|
||||
FirstName: "Alice",
|
||||
PushName: "Alice",
|
||||
}
|
||||
|
||||
base := time.Date(2024, 1, 2, 0, 0, 0, 0, time.UTC)
|
||||
|
||||
textMsg := &events.Message{
|
||||
Info: types.MessageInfo{
|
||||
MessageSource: types.MessageSource{
|
||||
Chat: chat,
|
||||
Sender: chat,
|
||||
IsFromMe: false,
|
||||
IsGroup: false,
|
||||
},
|
||||
ID: "m-text",
|
||||
Timestamp: base.Add(1 * time.Second),
|
||||
PushName: "Alice",
|
||||
},
|
||||
Message: &waProto.Message{Conversation: proto.String("hello")},
|
||||
}
|
||||
|
||||
imageMsg := &events.Message{
|
||||
Info: types.MessageInfo{
|
||||
MessageSource: types.MessageSource{
|
||||
Chat: chat,
|
||||
Sender: chat,
|
||||
IsFromMe: false,
|
||||
IsGroup: false,
|
||||
},
|
||||
ID: "m-image",
|
||||
Timestamp: base.Add(2 * time.Second),
|
||||
PushName: "Alice",
|
||||
},
|
||||
Message: &waProto.Message{
|
||||
ImageMessage: &waProto.ImageMessage{
|
||||
Mimetype: proto.String("image/jpeg"),
|
||||
DirectPath: proto.String("/direct"),
|
||||
MediaKey: []byte{1},
|
||||
FileSHA256: []byte{2},
|
||||
FileEncSHA256: []byte{3},
|
||||
FileLength: proto.Uint64(10),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
replyMsg := &events.Message{
|
||||
Info: types.MessageInfo{
|
||||
MessageSource: types.MessageSource{
|
||||
Chat: chat,
|
||||
Sender: chat,
|
||||
IsFromMe: false,
|
||||
IsGroup: false,
|
||||
},
|
||||
ID: "m-reply",
|
||||
Timestamp: base.Add(3 * time.Second),
|
||||
PushName: "Alice",
|
||||
},
|
||||
Message: &waProto.Message{
|
||||
ExtendedTextMessage: &waProto.ExtendedTextMessage{
|
||||
Text: proto.String("reply text"),
|
||||
ContextInfo: &waProto.ContextInfo{
|
||||
StanzaID: proto.String("m-text"),
|
||||
QuotedMessage: &waProto.Message{
|
||||
Conversation: proto.String("quoted text"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
reactionMsg := &events.Message{
|
||||
Info: types.MessageInfo{
|
||||
MessageSource: types.MessageSource{
|
||||
Chat: chat,
|
||||
Sender: chat,
|
||||
IsFromMe: false,
|
||||
IsGroup: false,
|
||||
},
|
||||
ID: "m-react",
|
||||
Timestamp: base.Add(4 * time.Second),
|
||||
PushName: "Alice",
|
||||
},
|
||||
Message: &waProto.Message{
|
||||
ReactionMessage: &waProto.ReactionMessage{
|
||||
Text: proto.String("👍"),
|
||||
Key: &waProto.MessageKey{ID: proto.String("m-text")},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
f.connectEvents = []interface{}{textMsg, imageMsg, replyMsg, reactionMsg}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
go func() {
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
cancel()
|
||||
}()
|
||||
res, err := a.Sync(ctx, SyncOptions{
|
||||
Mode: SyncModeFollow,
|
||||
AllowQR: false,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Sync: %v", err)
|
||||
}
|
||||
if res.MessagesStored != 4 {
|
||||
t.Fatalf("expected 4 MessagesStored, got %d", res.MessagesStored)
|
||||
}
|
||||
|
||||
msg, err := a.db.GetMessage(chat.String(), "m-text")
|
||||
if err != nil {
|
||||
t.Fatalf("GetMessage text: %v", err)
|
||||
}
|
||||
if msg.DisplayText != "hello" {
|
||||
t.Fatalf("expected display text 'hello', got %q", msg.DisplayText)
|
||||
}
|
||||
|
||||
msg, err = a.db.GetMessage(chat.String(), "m-image")
|
||||
if err != nil {
|
||||
t.Fatalf("GetMessage image: %v", err)
|
||||
}
|
||||
if msg.DisplayText != "Sent image" {
|
||||
t.Fatalf("expected display text 'Sent image', got %q", msg.DisplayText)
|
||||
}
|
||||
|
||||
msg, err = a.db.GetMessage(chat.String(), "m-reply")
|
||||
if err != nil {
|
||||
t.Fatalf("GetMessage reply: %v", err)
|
||||
}
|
||||
if msg.DisplayText != "> quoted text\nreply text" {
|
||||
t.Fatalf("unexpected reply display text: %q", msg.DisplayText)
|
||||
}
|
||||
|
||||
msg, err = a.db.GetMessage(chat.String(), "m-react")
|
||||
if err != nil {
|
||||
t.Fatalf("GetMessage react: %v", err)
|
||||
}
|
||||
if msg.DisplayText != "Reacted 👍 to hello" {
|
||||
t.Fatalf("unexpected reaction display text: %q", msg.DisplayText)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSyncOnceIdleExit(t *testing.T) {
|
||||
a := newTestApp(t)
|
||||
f := newFakeWA()
|
||||
|
||||
@ -25,6 +25,7 @@ func TestOpenCreatesExpectedSchema(t *testing.T) {
|
||||
for _, want := range []string{
|
||||
"chat_name",
|
||||
"sender_name",
|
||||
"display_text",
|
||||
"local_path",
|
||||
"downloaded_at",
|
||||
} {
|
||||
|
||||
@ -119,6 +119,7 @@ func (d *DB) ensureSchema() error {
|
||||
ts INTEGER NOT NULL,
|
||||
from_me INTEGER NOT NULL,
|
||||
text TEXT,
|
||||
display_text TEXT,
|
||||
media_type TEXT,
|
||||
media_caption TEXT,
|
||||
filename TEXT,
|
||||
@ -140,19 +141,67 @@ func (d *DB) ensureSchema() error {
|
||||
return fmt.Errorf("create tables: %w", err)
|
||||
}
|
||||
|
||||
if _, err := d.sql.Exec(`
|
||||
CREATE VIRTUAL TABLE IF NOT EXISTS messages_fts USING fts5(
|
||||
text,
|
||||
media_caption,
|
||||
filename,
|
||||
chat_name,
|
||||
sender_name
|
||||
);
|
||||
`); err != nil {
|
||||
// Continue without FTS (fallback to LIKE).
|
||||
d.ftsEnabled = false
|
||||
if err := d.ensureMessageColumns(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := d.ensureMessagesFTS(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *DB) ensureMessageColumns() error {
|
||||
ok, err := d.tableHasColumn("messages", "display_text")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if ok {
|
||||
return nil
|
||||
}
|
||||
if _, err := d.sql.Exec(`ALTER TABLE messages ADD COLUMN display_text TEXT`); err != nil {
|
||||
return fmt.Errorf("add display_text column: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *DB) ensureMessagesFTS() error {
|
||||
ftsExists, err := d.tableExists("messages_fts")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if ftsExists {
|
||||
hasDisplay, err := d.tableHasColumn("messages_fts", "display_text")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !hasDisplay {
|
||||
if _, err := d.sql.Exec(`DROP TABLE IF EXISTS messages_fts`); err != nil {
|
||||
return fmt.Errorf("drop messages_fts: %w", err)
|
||||
}
|
||||
ftsExists = false
|
||||
}
|
||||
}
|
||||
|
||||
created := false
|
||||
if !ftsExists {
|
||||
if _, err := d.sql.Exec(`
|
||||
CREATE VIRTUAL TABLE IF NOT EXISTS messages_fts USING fts5(
|
||||
text,
|
||||
media_caption,
|
||||
filename,
|
||||
chat_name,
|
||||
sender_name,
|
||||
display_text
|
||||
);
|
||||
`); err != nil {
|
||||
// Continue without FTS (fallback to LIKE).
|
||||
d.ftsEnabled = false
|
||||
return nil
|
||||
}
|
||||
created = true
|
||||
}
|
||||
|
||||
// Ensure triggers match our expected semantics (FTS5 supports DELETE directly).
|
||||
if _, err := d.sql.Exec(`
|
||||
@ -161,8 +210,8 @@ func (d *DB) ensureSchema() error {
|
||||
DROP TRIGGER IF EXISTS messages_au;
|
||||
|
||||
CREATE TRIGGER messages_ai AFTER INSERT ON messages BEGIN
|
||||
INSERT INTO messages_fts(rowid, text, media_caption, filename, chat_name, sender_name)
|
||||
VALUES (new.rowid, COALESCE(new.text,''), COALESCE(new.media_caption,''), COALESCE(new.filename,''), COALESCE(new.chat_name,''), COALESCE(new.sender_name,''));
|
||||
INSERT INTO messages_fts(rowid, text, media_caption, filename, chat_name, sender_name, display_text)
|
||||
VALUES (new.rowid, COALESCE(new.text,''), COALESCE(new.media_caption,''), COALESCE(new.filename,''), COALESCE(new.chat_name,''), COALESCE(new.sender_name,''), COALESCE(new.display_text,''));
|
||||
END;
|
||||
|
||||
CREATE TRIGGER messages_ad AFTER DELETE ON messages BEGIN
|
||||
@ -171,18 +220,70 @@ func (d *DB) ensureSchema() error {
|
||||
|
||||
CREATE TRIGGER messages_au AFTER UPDATE ON messages BEGIN
|
||||
DELETE FROM messages_fts WHERE rowid = old.rowid;
|
||||
INSERT INTO messages_fts(rowid, text, media_caption, filename, chat_name, sender_name)
|
||||
VALUES (new.rowid, COALESCE(new.text,''), COALESCE(new.media_caption,''), COALESCE(new.filename,''), COALESCE(new.chat_name,''), COALESCE(new.sender_name,''));
|
||||
INSERT INTO messages_fts(rowid, text, media_caption, filename, chat_name, sender_name, display_text)
|
||||
VALUES (new.rowid, COALESCE(new.text,''), COALESCE(new.media_caption,''), COALESCE(new.filename,''), COALESCE(new.chat_name,''), COALESCE(new.sender_name,''), COALESCE(new.display_text,''));
|
||||
END;
|
||||
`); err != nil {
|
||||
d.ftsEnabled = false
|
||||
return nil
|
||||
}
|
||||
|
||||
if created {
|
||||
if _, err := d.sql.Exec(`
|
||||
INSERT INTO messages_fts(rowid, text, media_caption, filename, chat_name, sender_name, display_text)
|
||||
SELECT rowid,
|
||||
COALESCE(text,''),
|
||||
COALESCE(media_caption,''),
|
||||
COALESCE(filename,''),
|
||||
COALESCE(chat_name,''),
|
||||
COALESCE(sender_name,''),
|
||||
COALESCE(display_text,'')
|
||||
FROM messages;
|
||||
`); err != nil {
|
||||
d.ftsEnabled = false
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
d.ftsEnabled = true
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *DB) tableExists(table string) (bool, error) {
|
||||
row := d.sql.QueryRow(`SELECT 1 FROM sqlite_master WHERE name = ? AND type IN ('table','view')`, table)
|
||||
var one int
|
||||
if err := row.Scan(&one); err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return false, nil
|
||||
}
|
||||
return false, err
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (d *DB) tableHasColumn(table, column string) (bool, error) {
|
||||
rows, err := d.sql.Query("PRAGMA table_info(" + table + ")")
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
var cid int
|
||||
var name string
|
||||
var colType string
|
||||
var notNull int
|
||||
var pk int
|
||||
var dflt sql.NullString
|
||||
if err := rows.Scan(&cid, &name, &colType, ¬Null, &dflt, &pk); err != nil {
|
||||
return false, err
|
||||
}
|
||||
if strings.EqualFold(name, column) {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
return false, rows.Err()
|
||||
}
|
||||
|
||||
// --- domain types + helpers
|
||||
|
||||
type Chat struct {
|
||||
@ -224,15 +325,16 @@ type MediaDownloadInfo struct {
|
||||
}
|
||||
|
||||
type Message struct {
|
||||
ChatJID string
|
||||
ChatName string
|
||||
MsgID string
|
||||
SenderJID string
|
||||
Timestamp time.Time
|
||||
FromMe bool
|
||||
Text string
|
||||
MediaType string
|
||||
Snippet string
|
||||
ChatJID string
|
||||
ChatName string
|
||||
MsgID string
|
||||
SenderJID string
|
||||
Timestamp time.Time
|
||||
FromMe bool
|
||||
Text string
|
||||
DisplayText string
|
||||
MediaType string
|
||||
Snippet string
|
||||
}
|
||||
|
||||
type MessageInfo struct {
|
||||
@ -298,6 +400,7 @@ type UpsertMessageParams struct {
|
||||
Timestamp time.Time
|
||||
FromMe bool
|
||||
Text string
|
||||
DisplayText string
|
||||
MediaType string
|
||||
MediaCaption string
|
||||
Filename string
|
||||
@ -312,10 +415,10 @@ type UpsertMessageParams struct {
|
||||
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,
|
||||
chat_jid, chat_name, msg_id, sender_jid, sender_name, ts, from_me, text, display_text,
|
||||
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,
|
||||
@ -323,6 +426,7 @@ func (d *DB) UpsertMessage(p UpsertMessageParams) error {
|
||||
ts=excluded.ts,
|
||||
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,
|
||||
media_type=excluded.media_type,
|
||||
media_caption=excluded.media_caption,
|
||||
filename=COALESCE(NULLIF(excluded.filename,''), messages.filename),
|
||||
@ -332,7 +436,7 @@ func (d *DB) UpsertMessage(p UpsertMessageParams) error {
|
||||
file_sha256=CASE WHEN excluded.file_sha256 IS NOT NULL AND length(excluded.file_sha256)>0 THEN excluded.file_sha256 ELSE messages.file_sha256 END,
|
||||
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),
|
||||
`, 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),
|
||||
nullIfEmpty(p.MediaType), nullIfEmpty(p.MediaCaption), nullIfEmpty(p.Filename), nullIfEmpty(p.MimeType), nullIfEmpty(p.DirectPath),
|
||||
p.MediaKey, p.FileSHA256, p.FileEncSHA256, int64(p.FileLength),
|
||||
)
|
||||
@ -359,7 +463,7 @@ func (d *DB) ListMessages(p ListMessagesParams) ([]Message, error) {
|
||||
p.Limit = 50
|
||||
}
|
||||
query := `
|
||||
SELECT m.chat_jid, COALESCE(c.name,''), m.msg_id, COALESCE(m.sender_jid,''), m.ts, m.from_me, COALESCE(m.text,''), COALESCE(m.media_type,'')
|
||||
SELECT 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,'')
|
||||
FROM messages m
|
||||
LEFT JOIN chats c ON c.jid = m.chat_jid
|
||||
WHERE 1=1`
|
||||
@ -390,7 +494,7 @@ func (d *DB) ListMessages(p ListMessagesParams) ([]Message, error) {
|
||||
var m Message
|
||||
var ts int64
|
||||
var fromMe int
|
||||
if err := rows.Scan(&m.ChatJID, &m.ChatName, &m.MsgID, &m.SenderJID, &ts, &fromMe, &m.Text, &m.MediaType); err != nil {
|
||||
if err := rows.Scan(&m.ChatJID, &m.ChatName, &m.MsgID, &m.SenderJID, &ts, &fromMe, &m.Text, &m.DisplayText, &m.MediaType); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
m.Timestamp = fromUnix(ts)
|
||||
@ -426,12 +530,12 @@ func (d *DB) SearchMessages(p SearchMessagesParams) ([]Message, error) {
|
||||
|
||||
func (d *DB) searchLIKE(p SearchMessagesParams) ([]Message, error) {
|
||||
query := `
|
||||
SELECT m.chat_jid, COALESCE(c.name,''), m.msg_id, COALESCE(m.sender_jid,''), m.ts, m.from_me, COALESCE(m.text,''), COALESCE(m.media_type,''), ''
|
||||
SELECT 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,''), ''
|
||||
FROM messages m
|
||||
LEFT JOIN chats c ON c.jid = m.chat_jid
|
||||
WHERE (LOWER(m.text) LIKE LOWER(?) OR LOWER(m.media_caption) LIKE LOWER(?) OR LOWER(m.filename) LIKE LOWER(?) OR LOWER(COALESCE(m.chat_name,'')) LIKE LOWER(?) OR LOWER(COALESCE(m.sender_name,'')) LIKE LOWER(?) OR LOWER(COALESCE(c.name,'')) LIKE LOWER(?))`
|
||||
WHERE (LOWER(m.text) LIKE LOWER(?) OR LOWER(m.display_text) LIKE LOWER(?) OR LOWER(m.media_caption) LIKE LOWER(?) OR LOWER(m.filename) LIKE LOWER(?) OR LOWER(COALESCE(m.chat_name,'')) LIKE LOWER(?) OR LOWER(COALESCE(m.sender_name,'')) LIKE LOWER(?) OR LOWER(COALESCE(c.name,'')) LIKE LOWER(?))`
|
||||
needle := "%" + p.Query + "%"
|
||||
args := []interface{}{needle, needle, needle, needle, needle, needle}
|
||||
args := []interface{}{needle, needle, needle, needle, needle, needle, needle}
|
||||
query, args = applyMessageFilters(query, args, p)
|
||||
query += " ORDER BY m.ts DESC LIMIT ?"
|
||||
args = append(args, p.Limit)
|
||||
@ -440,7 +544,7 @@ func (d *DB) searchLIKE(p SearchMessagesParams) ([]Message, error) {
|
||||
|
||||
func (d *DB) searchFTS(p SearchMessagesParams) ([]Message, error) {
|
||||
query := `
|
||||
SELECT m.chat_jid, COALESCE(c.name,''), m.msg_id, COALESCE(m.sender_jid,''), m.ts, m.from_me, COALESCE(m.text,''), COALESCE(m.media_type,''),
|
||||
SELECT 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,''),
|
||||
snippet(messages_fts, 0, '[', ']', '…', 12)
|
||||
FROM messages_fts
|
||||
JOIN messages m ON messages_fts.rowid = m.rowid
|
||||
@ -489,7 +593,7 @@ func (d *DB) scanMessages(query string, args ...interface{}) ([]Message, error)
|
||||
var m Message
|
||||
var ts int64
|
||||
var fromMe int
|
||||
if err := rows.Scan(&m.ChatJID, &m.ChatName, &m.MsgID, &m.SenderJID, &ts, &fromMe, &m.Text, &m.MediaType, &m.Snippet); err != nil {
|
||||
if err := rows.Scan(&m.ChatJID, &m.ChatName, &m.MsgID, &m.SenderJID, &ts, &fromMe, &m.Text, &m.DisplayText, &m.MediaType, &m.Snippet); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
m.Timestamp = fromUnix(ts)
|
||||
@ -501,7 +605,7 @@ func (d *DB) scanMessages(query string, args ...interface{}) ([]Message, error)
|
||||
|
||||
func (d *DB) GetMessage(chatJID, msgID string) (Message, error) {
|
||||
row := d.sql.QueryRow(`
|
||||
SELECT m.chat_jid, COALESCE(c.name,''), m.msg_id, COALESCE(m.sender_jid,''), m.ts, m.from_me, COALESCE(m.text,''), COALESCE(m.media_type,'')
|
||||
SELECT 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,'')
|
||||
FROM messages m
|
||||
LEFT JOIN chats c ON c.jid = m.chat_jid
|
||||
WHERE m.chat_jid = ? AND m.msg_id = ?
|
||||
@ -509,7 +613,7 @@ func (d *DB) GetMessage(chatJID, msgID string) (Message, error) {
|
||||
var m Message
|
||||
var ts int64
|
||||
var fromMe int
|
||||
if err := row.Scan(&m.ChatJID, &m.ChatName, &m.MsgID, &m.SenderJID, &ts, &fromMe, &m.Text, &m.MediaType); err != nil {
|
||||
if err := row.Scan(&m.ChatJID, &m.ChatName, &m.MsgID, &m.SenderJID, &ts, &fromMe, &m.Text, &m.DisplayText, &m.MediaType); err != nil {
|
||||
return Message{}, err
|
||||
}
|
||||
m.Timestamp = fromUnix(ts)
|
||||
@ -618,7 +722,7 @@ func (d *DB) MessageContext(chatJID, msgID string, before, after int) ([]Message
|
||||
}
|
||||
|
||||
beforeRows, err := d.sql.Query(`
|
||||
SELECT m.chat_jid, COALESCE(c.name,''), m.msg_id, COALESCE(m.sender_jid,''), m.ts, m.from_me, COALESCE(m.text,''), COALESCE(m.media_type,''), ''
|
||||
SELECT 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,''), ''
|
||||
FROM messages m
|
||||
LEFT JOIN chats c ON c.jid = m.chat_jid
|
||||
WHERE m.chat_jid = ? AND m.ts < ?
|
||||
@ -635,7 +739,7 @@ func (d *DB) MessageContext(chatJID, msgID string, before, after int) ([]Message
|
||||
var m Message
|
||||
var ts int64
|
||||
var fromMe int
|
||||
if err := beforeRows.Scan(&m.ChatJID, &m.ChatName, &m.MsgID, &m.SenderJID, &ts, &fromMe, &m.Text, &m.MediaType, &m.Snippet); err != nil {
|
||||
if err := beforeRows.Scan(&m.ChatJID, &m.ChatName, &m.MsgID, &m.SenderJID, &ts, &fromMe, &m.Text, &m.DisplayText, &m.MediaType, &m.Snippet); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
m.Timestamp = fromUnix(ts)
|
||||
@ -647,7 +751,7 @@ func (d *DB) MessageContext(chatJID, msgID string, before, after int) ([]Message
|
||||
}
|
||||
|
||||
afterRows, err := d.sql.Query(`
|
||||
SELECT m.chat_jid, COALESCE(c.name,''), m.msg_id, COALESCE(m.sender_jid,''), m.ts, m.from_me, COALESCE(m.text,''), COALESCE(m.media_type,''), ''
|
||||
SELECT 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,''), ''
|
||||
FROM messages m
|
||||
LEFT JOIN chats c ON c.jid = m.chat_jid
|
||||
WHERE m.chat_jid = ? AND m.ts > ?
|
||||
@ -664,7 +768,7 @@ func (d *DB) MessageContext(chatJID, msgID string, before, after int) ([]Message
|
||||
var m Message
|
||||
var ts int64
|
||||
var fromMe int
|
||||
if err := afterRows.Scan(&m.ChatJID, &m.ChatName, &m.MsgID, &m.SenderJID, &ts, &fromMe, &m.Text, &m.MediaType, &m.Snippet); err != nil {
|
||||
if err := afterRows.Scan(&m.ChatJID, &m.ChatName, &m.MsgID, &m.SenderJID, &ts, &fromMe, &m.Text, &m.DisplayText, &m.MediaType, &m.Snippet); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
m.Timestamp = fromUnix(ts)
|
||||
|
||||
@ -14,6 +14,7 @@ import (
|
||||
waProto "go.mau.fi/whatsmeow/binary/proto"
|
||||
"go.mau.fi/whatsmeow/store/sqlstore"
|
||||
"go.mau.fi/whatsmeow/types"
|
||||
"go.mau.fi/whatsmeow/types/events"
|
||||
waLog "go.mau.fi/whatsmeow/util/log"
|
||||
)
|
||||
|
||||
@ -206,6 +207,16 @@ func (c *Client) Upload(ctx context.Context, data []byte, mediaType whatsmeow.Me
|
||||
return cli.Upload(ctx, data, mediaType)
|
||||
}
|
||||
|
||||
func (c *Client) DecryptReaction(ctx context.Context, reaction *events.Message) (*waProto.ReactionMessage, error) {
|
||||
c.mu.Lock()
|
||||
cli := c.client
|
||||
c.mu.Unlock()
|
||||
if cli == nil || !cli.IsConnected() {
|
||||
return nil, fmt.Errorf("not connected")
|
||||
}
|
||||
return cli.DecryptReaction(ctx, reaction)
|
||||
}
|
||||
|
||||
func (c *Client) RequestHistorySyncOnDemand(ctx context.Context, lastKnown types.MessageInfo, count int) (types.MessageID, error) {
|
||||
c.mu.Lock()
|
||||
cli := c.client
|
||||
|
||||
@ -22,14 +22,18 @@ type Media struct {
|
||||
}
|
||||
|
||||
type ParsedMessage struct {
|
||||
Chat types.JID
|
||||
ID string
|
||||
SenderJID string
|
||||
Timestamp time.Time
|
||||
FromMe bool
|
||||
Text string
|
||||
Media *Media
|
||||
PushName 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
|
||||
}
|
||||
|
||||
func ParseLiveMessage(evt *events.Message) ParsedMessage {
|
||||
@ -78,6 +82,17 @@ func extractWAProto(m *waProto.Message, pm *ParsedMessage) {
|
||||
return
|
||||
}
|
||||
|
||||
if reaction := m.GetReactionMessage(); reaction != nil {
|
||||
pm.ReactionEmoji = reaction.GetText()
|
||||
if key := reaction.GetKey(); key != nil {
|
||||
pm.ReactionToID = key.GetID()
|
||||
}
|
||||
} else if encReaction := m.GetEncReactionMessage(); encReaction != nil {
|
||||
if key := encReaction.GetTargetMessageKey(); key != nil {
|
||||
pm.ReactionToID = key.GetID()
|
||||
}
|
||||
}
|
||||
|
||||
switch {
|
||||
case m.GetConversation() != "":
|
||||
pm.Text = m.GetConversation()
|
||||
@ -99,15 +114,18 @@ func extractWAProto(m *waProto.Message, pm *ParsedMessage) {
|
||||
FileEncSHA256: clone(img.GetFileEncSHA256()),
|
||||
FileLength: img.GetFileLength(),
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if vid := m.GetVideoMessage(); vid != nil {
|
||||
if pm.Text == "" {
|
||||
pm.Text = vid.GetCaption()
|
||||
}
|
||||
mediaType := "video"
|
||||
if vid.GetGifPlayback() {
|
||||
mediaType = "gif"
|
||||
}
|
||||
pm.Media = &Media{
|
||||
Type: "video",
|
||||
Type: mediaType,
|
||||
Caption: vid.GetCaption(),
|
||||
MimeType: vid.GetMimetype(),
|
||||
DirectPath: vid.GetDirectPath(),
|
||||
@ -116,7 +134,6 @@ func extractWAProto(m *waProto.Message, pm *ParsedMessage) {
|
||||
FileEncSHA256: clone(vid.GetFileEncSHA256()),
|
||||
FileLength: vid.GetFileLength(),
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if aud := m.GetAudioMessage(); aud != nil {
|
||||
@ -133,7 +150,6 @@ func extractWAProto(m *waProto.Message, pm *ParsedMessage) {
|
||||
FileEncSHA256: clone(aud.GetFileEncSHA256()),
|
||||
FileLength: aud.GetFileLength(),
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if doc := m.GetDocumentMessage(); doc != nil {
|
||||
@ -151,7 +167,27 @@ func extractWAProto(m *waProto.Message, pm *ParsedMessage) {
|
||||
FileEncSHA256: clone(doc.GetFileEncSHA256()),
|
||||
FileLength: doc.GetFileLength(),
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if sticker := m.GetStickerMessage(); sticker != nil {
|
||||
pm.Media = &Media{
|
||||
Type: "sticker",
|
||||
MimeType: sticker.GetMimetype(),
|
||||
DirectPath: sticker.GetDirectPath(),
|
||||
MediaKey: clone(sticker.GetMediaKey()),
|
||||
FileSHA256: clone(sticker.GetFileSHA256()),
|
||||
FileEncSHA256: clone(sticker.GetFileEncSHA256()),
|
||||
FileLength: sticker.GetFileLength(),
|
||||
}
|
||||
}
|
||||
|
||||
if ctx := contextInfoForMessage(m); ctx != nil {
|
||||
if id := strings.TrimSpace(ctx.GetStanzaID()); id != "" {
|
||||
pm.ReplyToID = id
|
||||
}
|
||||
if quoted := ctx.GetQuotedMessage(); quoted != nil {
|
||||
pm.ReplyToDisplay = strings.TrimSpace(displayTextForProto(quoted))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -163,3 +199,81 @@ func clone(b []byte) []byte {
|
||||
copy(out, b)
|
||||
return out
|
||||
}
|
||||
|
||||
func contextInfoForMessage(m *waProto.Message) *waProto.ContextInfo {
|
||||
if m == nil {
|
||||
return nil
|
||||
}
|
||||
if ext := m.GetExtendedTextMessage(); ext != nil {
|
||||
return ext.GetContextInfo()
|
||||
}
|
||||
if img := m.GetImageMessage(); img != nil {
|
||||
return img.GetContextInfo()
|
||||
}
|
||||
if vid := m.GetVideoMessage(); vid != nil {
|
||||
return vid.GetContextInfo()
|
||||
}
|
||||
if aud := m.GetAudioMessage(); aud != nil {
|
||||
return aud.GetContextInfo()
|
||||
}
|
||||
if doc := m.GetDocumentMessage(); doc != nil {
|
||||
return doc.GetContextInfo()
|
||||
}
|
||||
if sticker := m.GetStickerMessage(); sticker != nil {
|
||||
return sticker.GetContextInfo()
|
||||
}
|
||||
if loc := m.GetLocationMessage(); loc != nil {
|
||||
return loc.GetContextInfo()
|
||||
}
|
||||
if contact := m.GetContactMessage(); contact != nil {
|
||||
return contact.GetContextInfo()
|
||||
}
|
||||
if contacts := m.GetContactsArrayMessage(); contacts != nil {
|
||||
return contacts.GetContextInfo()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func displayTextForProto(m *waProto.Message) string {
|
||||
if m == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
if img := m.GetImageMessage(); img != nil {
|
||||
return "Sent image"
|
||||
}
|
||||
if vid := m.GetVideoMessage(); vid != nil {
|
||||
if vid.GetGifPlayback() {
|
||||
return "Sent gif"
|
||||
}
|
||||
return "Sent video"
|
||||
}
|
||||
if aud := m.GetAudioMessage(); aud != nil {
|
||||
return "Sent audio"
|
||||
}
|
||||
if doc := m.GetDocumentMessage(); doc != nil {
|
||||
return "Sent document"
|
||||
}
|
||||
if sticker := m.GetStickerMessage(); sticker != nil {
|
||||
return "Sent sticker"
|
||||
}
|
||||
if loc := m.GetLocationMessage(); loc != nil {
|
||||
return "Sent location"
|
||||
}
|
||||
if contact := m.GetContactMessage(); contact != nil {
|
||||
return "Sent contact"
|
||||
}
|
||||
if contacts := m.GetContactsArrayMessage(); contacts != nil {
|
||||
return "Sent contacts"
|
||||
}
|
||||
|
||||
if text := strings.TrimSpace(m.GetConversation()); text != "" {
|
||||
return text
|
||||
}
|
||||
if ext := m.GetExtendedTextMessage(); ext != nil {
|
||||
if text := strings.TrimSpace(ext.GetText()); text != "" {
|
||||
return text
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
@ -74,3 +74,69 @@ func TestParseLiveMessageImageClonesBytes(t *testing.T) {
|
||||
t.Fatalf("expected MediaKey to be cloned")
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseLiveMessageReaction(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{
|
||||
ReactionMessage: &waProto.ReactionMessage{
|
||||
Text: proto.String("👍"),
|
||||
Key: &waProto.MessageKey{ID: proto.String("orig")},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
pm := ParseLiveMessage(ev)
|
||||
if pm.ReactionEmoji != "👍" || pm.ReactionToID != "orig" {
|
||||
t.Fatalf("unexpected reaction parse: %+v", pm)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseLiveMessageReply(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("reply text"),
|
||||
ContextInfo: &waProto.ContextInfo{
|
||||
StanzaID: proto.String("orig"),
|
||||
QuotedMessage: &waProto.Message{
|
||||
Conversation: proto.String("quoted"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
pm := ParseLiveMessage(ev)
|
||||
if pm.ReplyToID != "orig" {
|
||||
t.Fatalf("expected ReplyToID to be orig, got %q", pm.ReplyToID)
|
||||
}
|
||||
if pm.ReplyToDisplay != "quoted" {
|
||||
t.Fatalf("expected ReplyToDisplay to be quoted, got %q", pm.ReplyToDisplay)
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user