diff --git a/internal/discorddesktop/import.go b/internal/discorddesktop/import.go index cccc2c9..d89b14b 100644 --- a/internal/discorddesktop/import.go +++ b/internal/discorddesktop/import.go @@ -348,6 +348,7 @@ func collectValue(snap snapshot, channelLookup map[string]store.ChannelRecord, v switch typed := value.(type) { case map[string]any: collectUserLabel(snap, typed) + collectSelectedDirectMessageRoutes(snap, typed) if channel, ok := parseChannel(typed); ok { snap.channels[channel.ID] = channel channelLookup[channel.ID] = channel @@ -380,14 +381,46 @@ func collectChannelRoutes(snap snapshot, data []byte) { if !looksSnowflake(channelID) { continue } - if existing, ok := snap.routes[channelID]; ok && existing != guildID { - snap.routes[channelID] = "" - continue - } - snap.routes[channelID] = guildID + collectChannelRoute(snap, channelID, guildID) } } +func collectSelectedDirectMessageRoutes(snap snapshot, raw map[string]any) { + for _, candidate := range selectedChannelRouteCandidates(raw) { + if selected, _ := candidate["selectedChannelIds"].(map[string]any); selected != nil { + if channelID := stringField(selected, "null"); looksSnowflake(channelID) { + collectChannelRoute(snap, channelID, DirectMessageGuildID) + } + } + if guildValue, hasGuild := candidate["selectedGuildId"]; hasGuild && guildValue == nil { + if channelID := stringField(candidate, "selectedChannelId"); looksSnowflake(channelID) { + collectChannelRoute(snap, channelID, DirectMessageGuildID) + } + } + } +} + +func selectedChannelRouteCandidates(raw map[string]any) []map[string]any { + candidates := []map[string]any{raw} + for _, key := range []string{"_state", "state"} { + if child, _ := raw[key].(map[string]any); child != nil { + candidates = append(candidates, child) + } + } + return candidates +} + +func collectChannelRoute(snap snapshot, channelID, guildID string) { + if !looksSnowflake(channelID) || guildID == "" { + return + } + if existing, ok := snap.routes[channelID]; ok && existing != guildID { + snap.routes[channelID] = "" + return + } + snap.routes[channelID] = guildID +} + func parseChannel(raw map[string]any) (store.ChannelRecord, bool) { id := stringField(raw, "id") if !looksSnowflake(id) { diff --git a/internal/discorddesktop/import_test.go b/internal/discorddesktop/import_test.go index 83ddb6b..5164016 100644 --- a/internal/discorddesktop/import_test.go +++ b/internal/discorddesktop/import_test.go @@ -228,6 +228,54 @@ func TestImportExtractsCompressedUnknownMessageArrayFromChromiumCache(t *testing require.Empty(t, results) } +func TestImportClassifiesCachedAPIMessageArrayFromSelectedDMRoute(t *testing.T) { + ctx := context.Background() + dir := t.TempDir() + cachePath := filepath.Join(dir, "Cache", "Cache_Data") + storagePath := filepath.Join(dir, "Local Storage", "leveldb") + require.NoError(t, os.MkdirAll(cachePath, 0o755)) + require.NoError(t, os.MkdirAll(storagePath, 0o755)) + + require.NoError(t, os.WriteFile(filepath.Join(storagePath, "000001.log"), []byte(`noise +{"_state":{"selectedGuildId":null,"selectedChannelId":"1459084628458471569","selectedChannelIds":{"null":"1459084628458471569"}}} +`), 0o600)) + + messages := `[ +{"id":"1499513741308461240","channel_id":"1459084628458471569","content":"changed your mind later","timestamp":"2026-04-30T20:52:15.546Z","author":{"id":"1395396685148061737","username":"onur_tc","global_name":"onur"}}, +{"id":"1499513741308461241","channel_id":"1459084628458471569","content":"please correct me","timestamp":"2026-04-30T20:52:16.546Z","author":{"id":"1395396685148061737","username":"onur_tc","global_name":"onur"}}, +{"id":"1499562787343278080","channel_id":"1459084628458471569","content":"I know you are going through a rough time","timestamp":"2026-05-01T00:08:34.929Z","author":{"id":"999999999999999991","username":"steipete","global_name":"Peter"}} +]` + var compressed bytes.Buffer + zw := gzip.NewWriter(&compressed) + _, err := zw.Write([]byte(messages)) + require.NoError(t, err) + require.NoError(t, zw.Close()) + + cacheBlob := append([]byte("https://discord.com/api/v9/channels/1459084628458471569/messages?limit=14\x00"), compressed.Bytes()...) + cacheBlob = append(cacheBlob, []byte("chromium trailing metadata")...) + require.NoError(t, os.WriteFile(filepath.Join(cachePath, "entry_0"), cacheBlob, 0o600)) + + dbPath := filepath.Join(dir, "discrawl.db") + st, err := store.Open(ctx, dbPath) + require.NoError(t, err) + defer func() { _ = st.Close() }() + + stats, err := Import(ctx, st, Options{Path: dir}) + require.NoError(t, err) + require.Equal(t, 2, stats.FilesScanned) + require.Equal(t, 3, stats.Messages) + require.Equal(t, 3, stats.DMMessages) + require.Equal(t, 1, stats.DMChannels) + require.Equal(t, 0, stats.SkippedMessages) + + results, err := st.SearchMessages(ctx, store.SearchOptions{Query: "changed your mind", Limit: 10}) + require.NoError(t, err) + require.Len(t, results, 1) + require.Equal(t, DirectMessageGuildID, results[0].GuildID) + require.Equal(t, "onur", results[0].ChannelName) + require.Equal(t, "onur", results[0].AuthorName) +} + func TestImportReconcilesMessagesWithLaterGuildChannelMetadata(t *testing.T) { ctx := context.Background() dir := t.TempDir()