fix(im): parse Drafty entities for message type classification (#31)

Favorites from Drafty messages (link/file/image/video/voice) always fell back to text because imMessageType only recognized flat file_id/url content. Derive the type from Drafty ent[] entities: LN->link, IM->image, VD->video, AU->voice, EX refined by mime. Attachments win over inline links.
This commit is contained in:
Austin
2026-09-20 16:38:51 +08:00
parent 48b3369799
commit 532ff49737
2 changed files with 167 additions and 0 deletions
+60
View File
@@ -8885,10 +8885,70 @@ func imMessageType(msg *types.Message) string {
if stringFromAny(content["url"]) != "" { if stringFromAny(content["url"]) != "" {
return "link" return "link"
} }
if mtype := draftyIMMessageType(content); mtype != "" {
return mtype
}
} }
return "text" return "text"
} }
// draftyIMMessageType derives the semantic message type from Drafty content
// ({"txt","fmt","ent"}), where "ent" holds entity definitions {"tp","data"}.
// Attachment entities win over inline links; EX is refined by its mime type.
// Returns "" when no recognized entity is present.
func draftyIMMessageType(content map[string]any) string {
ents, ok := content["ent"].([]any)
if !ok || len(ents) == 0 {
return ""
}
link := ""
file := ""
for _, e := range ents {
ent, ok := e.(map[string]any)
if !ok {
continue
}
data, _ := ent["data"].(map[string]any)
switch stringFromAny(ent["tp"]) {
case "IM":
return "image"
case "VD":
return "video"
case "AU":
return "voice"
case "EX":
if file == "" {
file = draftyAttachmentType(data)
}
case "LN":
if link == "" {
link = "link"
}
}
}
if file != "" {
return file
}
return link
}
func draftyAttachmentType(data map[string]any) string {
mime := ""
if data != nil {
mime = stringFromAny(data["mime"])
}
switch {
case strings.HasPrefix(mime, "image/"):
return "image"
case strings.HasPrefix(mime, "video/"):
return "video"
case strings.HasPrefix(mime, "audio/"):
return "voice"
default:
return "file"
}
}
func favoriteSnapshot(msg *types.Message) map[string]any { func favoriteSnapshot(msg *types.Message) map[string]any {
if msg == nil { if msg == nil {
return map[string]any{} return map[string]any{}
+107
View File
@@ -8002,3 +8002,110 @@ func ptrTime(t time.Time) *time.Time {
func ptrString(s string) *string { func ptrString(s string) *string {
return &s return &s
} }
func TestIMMessageTypeDraftyEntities(t *testing.T) {
cases := []struct {
name string
msg *types.Message
want string
}{
{
name: "nil message",
msg: nil,
want: "text",
},
{
name: "drafty link entity",
msg: &types.Message{Content: map[string]any{
"txt": "check this https://example.com",
"fmt": []any{map[string]any{"at": 10, "len": 19, "key": 0}},
"ent": []any{map[string]any{"tp": "LN", "data": map[string]any{"url": "https://example.com"}}},
}},
want: "link",
},
{
name: "drafty image entity",
msg: &types.Message{Content: map[string]any{
"txt": "a photo",
"fmt": []any{map[string]any{"at": 7, "len": 1, "key": 0}},
"ent": []any{map[string]any{"tp": "IM", "data": map[string]any{"mime": "image/png", "name": "pic.png"}}},
}},
want: "image",
},
{
name: "drafty video entity",
msg: &types.Message{Content: map[string]any{
"txt": "clip",
"ent": []any{map[string]any{"tp": "VD", "data": map[string]any{"mime": "video/mp4"}}},
}},
want: "video",
},
{
name: "drafty audio entity",
msg: &types.Message{Content: map[string]any{
"txt": "",
"ent": []any{map[string]any{"tp": "AU", "data": map[string]any{"mime": "audio/mp4"}}},
}},
want: "voice",
},
{
name: "drafty generic file entity",
msg: &types.Message{Content: map[string]any{
"txt": "doc",
"ent": []any{map[string]any{"tp": "EX", "data": map[string]any{"mime": "application/pdf", "name": "a.pdf"}}},
}},
want: "file",
},
{
name: "drafty image file entity refines by mime",
msg: &types.Message{Content: map[string]any{
"ent": []any{map[string]any{"tp": "EX", "data": map[string]any{"mime": "image/jpeg"}}},
}},
want: "image",
},
{
name: "drafty attachment wins over inline link",
msg: &types.Message{Content: map[string]any{
"txt": "see https://example.com and",
"fmt": []any{map[string]any{"at": 4, "len": 18, "key": 1}, map[string]any{"at": 26, "len": 1, "key": 0}},
"ent": []any{map[string]any{"tp": "EX", "data": map[string]any{"mime": "application/zip"}}, map[string]any{"tp": "LN", "data": map[string]any{}}},
}},
want: "file",
},
{
name: "drafty plain text",
msg: &types.Message{Content: map[string]any{
"txt": "hello",
}},
want: "text",
},
{
name: "legacy flat file_id content still works",
msg: &types.Message{Content: map[string]any{"file_id": "file-1", "mime": "image/png"}},
want: "image",
},
{
name: "legacy flat url content still works",
msg: &types.Message{Content: map[string]any{"url": "https://example.com"}},
want: "link",
},
{
name: "head type takes priority",
msg: &types.Message{
Head: types.KVMap{"type": "system_notice"},
Content: map[string]any{"ent": []any{map[string]any{"tp": "IM"}}},
},
want: "system_notice",
},
{
name: "string content",
msg: &types.Message{Content: "plain"},
want: "text",
},
}
for _, tc := range cases {
if got := imMessageType(tc.msg); got != tc.want {
t.Errorf("%s: imMessageType = %q, want %q", tc.name, got, tc.want)
}
}
}