diff --git a/server/im_http.go b/server/im_http.go index 184682f..43fdce5 100644 --- a/server/im_http.go +++ b/server/im_http.go @@ -8885,10 +8885,70 @@ func imMessageType(msg *types.Message) string { if stringFromAny(content["url"]) != "" { return "link" } + if mtype := draftyIMMessageType(content); mtype != "" { + return mtype + } } 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 { if msg == nil { return map[string]any{} diff --git a/server/im_http_test.go b/server/im_http_test.go index 9d63484..15598d1 100644 --- a/server/im_http_test.go +++ b/server/im_http_test.go @@ -8002,3 +8002,110 @@ func ptrTime(t time.Time) *time.Time { func ptrString(s string) *string { 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) + } + } +}